# Code Traveler > Stories from a traveling developer Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages ### About Code Traveler URL: https://codetraveler.io/about/ Last updated: 2023-08-22T17:58:12.000Z ![Brandon4](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/05/Brandon4.png) I am [Brandon Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io), and I work at AWS as a .NET Developer Advocate. Code Traveler combines my love for travel and code! Blog posts contains two topics: - **Code** The fun code I'm working on - **Traveler** A snippet about wherever I happened to be in the world ### Speaking URL: https://codetraveler.io/speaking/ Last updated: 2019-08-10T21:59:22.000Z I love to meet fellow developers around the world and geek out about code, and I'd love to join you at your event! [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) ### Upcoming Events - September 2019 - TechoramaNL - [https://www.techorama.nl/speakers/](https://www.techorama.nl/speakers/?ref=codetraveler.io) - October 2019 - VSLive Chicago - [https://vslive.com/Events/Chicago-2019/Sessions/Sunday/HOL02-HandsOn-Lab-Azure-and-Xamarin-Build-the-Mobile-Apps-of-Tomorrow.aspx](https://vslive.com/Events/Chicago-2019/Sessions/Sunday/HOL02-HandsOn-Lab-Azure-and-Xamarin-Build-the-Mobile-Apps-of-Tomorrow.aspx?ref=codetraveler.io) - Code Motion Milan - [https://events.codemotion.com/conferences/milan/2019/speaker/5670/](https://events.codemotion.com/conferences/milan/2019/speaker/5670/?ref=codetraveler.io) - November 2019 - Update Prague - [https://www.updateconference.net/en](https://www.updateconference.net/en?ref=codetraveler.io) ### Upgrade Your Apps With AI, Using Cognitive Services URL: https://codetraveler.io/mdevcamp2018/ Last updated: 2018-08-20T19:18:03.000Z mDevCamp, 15 June 2018 [https://mdevcamp.eu](https://mdevcamp.eu/?ref=codetraveler.io) ## Recording [https://slideslive.com/38908247/upgrade-your-apps-with-ai-using-cognitive-services](https://slideslive.com/38908247/upgrade-your-apps-with-ai-using-cognitive-services?ref=codetraveler.io) ## Cognitive Services Links - [Free $200 Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=mDevCamp-CodeTraveler-bramin&ref=codetraveler.io) - [Cognitive Services Docs](https://azure.microsoft.com/services/cognitive-services/?WT.mc%5Fid=mDevCamp-CodeTraveler-bramin&ref=codetraveler.io) - [Face API](https://azure.microsoft.com/en-us/services/cognitive-services/face/?WT.mc%5Fid=mDevCamp-CodeTraveler-bramin&ref=codetraveler.io) for Emotion Detection - [Text Analytics API](https://azure.microsoft.com/en-us/services/cognitive-services/text-analytics/?WT.mc%5Fid=mDevCamp-CodeTraveler-bramin&ref=codetraveler.io) for Sentiment Analysis ## Demo Apps - [FaceOff](https://github.com/brminnick/FaceOff?ref=codetraveler.io) - [HackerNews](https://github.com/brminnick/HackerNews?ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@BrandonXamarin](https://twitter.com/BrandonXamarin?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ## Photos ![36036773_2109558309332988_802562983409483776_o](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/36036773_2109558309332988_802562983409483776_o.jpg) James Montemagno and I chatting backstage before the keynote ![35852509_2109559225999563_721859792003399680_o](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/35852509_2109559225999563_721859792003399680_o.jpg) On the main stage ### Upgrading Your Apps with AI, Using Google Cloud URL: https://codetraveler.io/googlecloudai/ Last updated: 2018-08-20T19:18:03.000Z 25 June 2018 ## Slides [https://www.slideshare.net/secret/FHnz68h5AIHaFP](https://www.slideshare.net/secret/FHnz68h5AIHaFP?ref=codetraveler.io) ## Google Cloud AI Links - [Google Cloud AI](https://cloud.google.com/products/machine-learning/?ref=codetraveler.io) - [Cloud Vision API](https://cloud.google.com/vision/?ref=codetraveler.io) for Emotion Detection - [Natural Language API](https://cloud.google.com/natural-language/?ref=codetraveler.io) for Text Sentiment Analysis ## Demo Apps - [FaceOff](https://github.com/brminnick/FaceOff?ref=codetraveler.io) - [HackerNews](https://github.com/brminnick/HackerNews/tree/google-natural-language-api?ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@BrandonXamarin](https://twitter.com/BrandonXamarin?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Xamarin + IoT Workshop, SF URL: https://codetraveler.io/xamariniotworkshopsf/ Last updated: 2018-08-20T19:18:03.000Z @ The Microsoft Reactor SF [https://developer.microsoft.com/reactor/](https://developer.microsoft.com/reactor/?WT.mc%5Fid=XamarinIoTWorkshop-CodeTraveler-bramin&ref=codetraveler.io) ## Walkthrough + Source Code [https://github.com/brminnick/XamarinIoTWorkshop](https://github.com/brminnick/XamarinIoTWorkshop?ref=codetraveler.io) ## Slides - [Intro to Xamarin](https://www.slideshare.net/secret/LNx8F4I3xcw5n3?ref=codetraveler.io) - [Azure IoT](https://aka.ms/sfiotworkshop?ref=codetraveler.io) ## Survey [https://aka.ms/xamarinIoT](https://aka.ms/xamarinIoT?ref=codetraveler.io) ## Azure Links - [Free $200 Azure Credit](https://azure.microsoft.com/en-us/free/dotnet/?WT.mc%5Fid=XamarinIoTWorkshop-CodeTraveler-bramin&ref=codetraveler.io) - [Azure IoT Central](https://azure.microsoft.com/services/iot-central/?WT.mc%5Fid=XamarinIoTWorkshop-CodeTraveler-bramin&ref=codetraveler.io) - [Azure IoT Hub](https://azure.microsoft.com/services/iot-hub/?WT.mc%5Fid=XamarinIoTWorkshop-CodeTraveler-bramin&ref=codetraveler.io) ## Xamarin Links - [Getting Started](https://docs.microsoft.com/xamarin/?WT.mc%5Fid=XamarinIoTWorkshop-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin.Essentials](https://docs.microsoft.com/xamarin/essentials/?WT.mc%5Fid=XamarinIoTWorkshop-CodeTraveler-bramin&ref=codetraveler.io) ### Brandon Minnick - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - [Blog](https://wwww.codetraveler.io/?ref=codetraveler.io) ### Suz Hinton - [Website](http://noopkat.com/?ref=codetraveler.io) - [@NoopKat](https://twitter.com/noopkat?lang=en&ref=codetraveler.io) - [YouTube Channel](https://www.youtube.com/channel/UCoFU24KMXmCi4Sl3KIFPSVg?ref=codetraveler.io) - [Twitch Channel](https://www.twitch.tv/noopkat?ref=codetraveler.io) ### Implementing Cognitive Services using Xamarin URL: https://codetraveler.io/vegasready2018/ Last updated: 2018-08-20T19:18:03.000Z Microsoft Ready, Vegas 2018 ## Recording [https://content.microsoftready.com/FY19Q1/session/DAI-AAI310](https://content.microsoftready.com/FY19Q1/session/DAI-AAI310?ref=codetraveler.io) ## Slides [https://www.slideshare.net/secret/lyNyYDTwtv4BmG](https://www.slideshare.net/secret/lyNyYDTwtv4BmG?ref=codetraveler.io) ## Resources - [$200 Free Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) - [Cognitive Services Docs](https://azure.microsoft.com/services/cognitive-services/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) - [Face API](https://azure.microsoft.com/services/cognitive-services/face/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) - [FaceOff](https://github.com/brminnick/FaceOff?ref=codetraveler.io) - [Facial Recognition Login](https://github.com/brminnick/FacialRecognitionLogin?ref=codetraveler.io) - [Text Analytics API](https://azure.microsoft.com/services/cognitive-services/text-analytics/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) for Text Sentiment Analysis - [HackerNews](https://github.com/brminnick/HackerNews?ref=codetraveler.io) ## Contact Information Let's stay in touch! - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - [LinkedIn](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Correcting Common Mistakes When Using Async/Await in .NET URL: https://codetraveler.io/that-2018-asyncawait/ Last updated: 2019-07-15T16:18:10.000Z THAT Conference, 06 August 2018 [https://www.thatconference.com/speakers/speaker/bminnick](https://www.thatconference.com/speakers/speaker/bminnick?ref=codetraveler.io) ## Recording - [https://www.youtube.com/watch?v=J0mcYVxJEl0](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io) ## Slides - [https://www.slideshare.net/secret/Ehp0RSBb51NcVK](https://www.slideshare.net/secret/Ehp0RSBb51NcVK?ref=codetraveler.io) ## Demo App - [Hacker News](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io) ## Additional Learning - [Asynchronous Programming in .NET](https://docs.microsoft.com/dotnet/csharp/async?WT.mc%5Fid=THATConference-CodeTraveler-bramin&ref=codetraveler.io) - [Async/Await Best Practices Video](https://channel9.msdn.com/Shows/On-NET/Brandon-Minnick-asyncawait-best-practices/?WT.mc%5Fid=THATConference-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin Univeristy, CSC350](https://university.xamarin.com/classes/track/csharp?ref=codetraveler.io#csc350-async) - Using Async Await ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ## Photos ![martinezmedia-thatconference-20180806070-5-proof_44029538572_o](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/08/martinezmedia-thatconference-20180806070-5-proof_44029538572_o.jpg) ### Upgrading Your Apps with Cognitive Services URL: https://codetraveler.io/innovationwebinar/ Last updated: 2018-09-06T20:21:41.000Z Innovation Webinar, September 2018 ## Slides [https://www.slideshare.net/secret/40DsuYZ2963TWx](https://www.slideshare.net/secret/40DsuYZ2963TWx?ref=codetraveler.io) ## Resources - [$200 Free Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) - [Cognitive Services Docs](https://azure.microsoft.com/services/cognitive-services/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) - [Face API](https://azure.microsoft.com/services/cognitive-services/face/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) : [FaceOff](https://github.com/brminnick/FaceOff?ref=codetraveler.io), [Facial Recognition Login](https://github.com/brminnick/FacialRecognitionLogin?ref=codetraveler.io) - [Text Analytics API](https://azure.microsoft.com/services/cognitive-services/text-analytics/?WT.mc%5Fid=Ready-CodeTraveler-bramin&ref=codetraveler.io) for Text Sentiment Analysis: [HackerNews](https://github.com/brminnick/HackerNews?ref=codetraveler.io) ## Contact Information Let's stay in touch! - Brandon.Minnick@Microsoft.com - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - [LinkedIn](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Easily Add Artificial Intelligence To Your Apps URL: https://codetraveler.io/ndcsydney2018/ Last updated: 2019-01-21T09:59:14.000Z [https://ndcsydney.com/speaker/brandon-minnick/](https://ndcsydney.com/speaker/brandon-minnick/?ref=codetraveler.io) ## Recording [https://www.youtube.com/watch?v=OWdNKrPjbo8](https://www.youtube.com/watch?v=OWdNKrPjbo8&ref=codetraveler.io) ## Slides [https://www.slideshare.net/secret/w68fwYmOM76TP](https://www.slideshare.net/secret/w68fwYmOM76TP?ref=codetraveler.io) ## Azure Resources - [$200 Free Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=NDCSydney-CodeTraveler-bramin&ref=codetraveler.io) - [Cognitive Services Docs](https://azure.microsoft.com/services/cognitive-services/?WT.mc%5Fid=NDCSydney-CodeTraveler-bramin&ref=codetraveler.io) - [Face API](https://azure.microsoft.com/services/cognitive-services/face/?WT.mc%5Fid=NDCSydney-CodeTraveler-bramin&ref=codetraveler.io) - [Text Analytics API](https://azure.microsoft.com/services/cognitive-services/text-analytics/?WT.mc%5Fid=NDCSydney-CodeTraveler-bramin&ref=codetraveler.io) for Text Sentiment Analysis ## Google Cloud Resources - [Google Cloud AI](https://cloud.google.com/products/machine-learning/?ref=codetraveler.io) - [Cloud Vision API](https://cloud.google.com/vision/?ref=codetraveler.io) for Emotion Detection - [Natural Language API](https://cloud.google.com/natural-language/?ref=codetraveler.io) for Text Sentiment Analysis ## Demo Apps - [FaceOff](https://github.com/brminnick/FaceOff?ref=codetraveler.io) - [Facial Recognition Login](https://github.com/brminnick/FacialRecognitionLogin?ref=codetraveler.io) - [HackerNews](https://github.com/brminnick/HackerNews?ref=codetraveler.io) ## Contact Information Let's stay in touch! - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - [LinkedIn](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Correcting Common Async/Await Mistakes in .NET URL: https://codetraveler.io/ndcsydney-2018-asyncawait/ Last updated: 2019-07-15T16:16:56.000Z NDC Sydney 2018 [https://ndcsydney.com/speaker/brandon-minnick/](https://ndcsydney.com/speaker/brandon-minnick/?ref=codetraveler.io) ## Recording (Updated from NDC Oslo 2019) - [https://www.youtube.com/watch?v=J0mcYVxJEl0](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io) ## Slides - [https://www.slideshare.net/secret/18GQCQQwomqgAA](https://www.slideshare.net/secret/18GQCQQwomqgAA?ref=codetraveler.io) ## Demo App - [Hacker News](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io) ## Additional Learning - [Asynchronous Programming in .NET](https://docs.microsoft.com/dotnet/csharp/async?WT.mc%5Fid=NDCSydney-CodeTraveler-bramin&ref=codetraveler.io) - [Async/Await Best Practices Video](https://channel9.msdn.com/Shows/On-NET/Brandon-Minnick-asyncawait-best-practices/?WT.mc%5Fid=NDCSydney-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin Univeristy, CSC350](https://university.xamarin.com/classes/track/csharp?ref=codetraveler.io#csc350-async) - Using Async Await ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Correcting Common Async/Await Mistakes in .NET URL: https://codetraveler.io/devfestweekend-asyncawait/ Last updated: 2019-07-15T16:17:52.000Z Dev Fest Weekend 2018 [https://www.devfestweekend.com/](https://www.devfestweekend.com/?ref=codetraveler.io) ## Recording - [https://www.youtube.com/watch?v=J0mcYVxJEl0](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io) ## Slides - [https://www.slideshare.net/secret/zUpGE9ChK9LRNq](https://www.slideshare.net/secret/zUpGE9ChK9LRNq?ref=codetraveler.io) ## Demo App - [Hacker News](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io) ## Additional Learning - [Asynchronous Programming in .NET](https://docs.microsoft.com/dotnet/csharp/async?WT.mc%5Fid=DevFestWeekend-CodeTraveler-bramin&ref=codetraveler.io) - [The Managed Thread Pool](https://docs.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool?ref=codetraveler.io) - [Async/Await Best Practices Video](https://channel9.msdn.com/Shows/On-NET/Brandon-Minnick-asyncawait-best-practices/?WT.mc%5Fid=DevFestWeekend-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin University, CSC350](https://university.xamarin.com/classes/track/csharp?ref=codetraveler.io#csc350-async) - Using Async Await ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Intro to Xamarin URL: https://codetraveler.io/wvu-introtoxamarin/ Last updated: 2018-10-30T22:25:59.000Z WVU Guest Lecture, 30 October 2018 ## Xamarin Installation - Installation Walkthrough - [PDF](https://github.com/xamarin/dev-days-labs/blob/master/Xamarin%20Workshop%20Setup.pdf?ref=codetraveler.io) - [Word Doc](https://onedrive.live.com/view.aspx?ref=button&Bsrc=SMIT&resid=FB0D8F97004979CD!805726&cid=fb0d8f97004979cd&app=Word&authkey=!Aufl2efXW-5VLrg) ## Xamarin Sample Apps - [https://github.com/brminnick/dev-days-labs/tree/master/Demos](https://github.com/brminnick/dev-days-labs/tree/master/Demos?ref=codetraveler.io) ## Intro to Xamarin Slides - [https://www.slideshare.net/secret/BwcYZKZT1ecJKn](https://www.slideshare.net/secret/BwcYZKZT1ecJKn?ref=codetraveler.io) ## Microsoft Azure Cloud Links - [Free $200 Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=WVUGuestLecture-CodeTraveler-bramin&ref=codetraveler.io) - [Free Azure Account for Students](https://azure.microsoft.com/free/students/?WT.mc%5Fid=WVUGuestLecture-CodeTraveler-bramin&ref=codetraveler.io) - Includes $100 Free Credit - No credit card required - [Azure for Students FAQ](https://azure.microsoft.com/free/students-starter-faq/?WT.mc%5Fid=WVUGuestLecture-CodeTraveler-bramin&ref=codetraveler.io) ## Xamarin Links - [Xamarin Docs](https://docs.microsoft.com/xamarin/?WT.mc%5Fid=WVUGuestLecture-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin University](https://university.xamarin.com/?ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@BrandonXamarin](https://twitter.com/BrandonXamarin?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### What’s New in Tizen .NET? URL: https://codetraveler.io/sdc-tizen-net/ Last updated: 2019-04-03T17:24:50.000Z Samsung Developer Conference, November 2018 ## Recording [https://www.youtube.com/watch?v=eHyHHoK2vuo](https://www.youtube.com/watch?v=eHyHHoK2vuo&ref=codetraveler.io) ## Slides - [Native Apps with .NET + Xamarin.Forms](https://www.slideshare.net/secret/xVQ8cB1jQZqdr0?ref=codetraveler.io) - [What's New in Tizen .NET](https://www.slideshare.net/secret/2RfI9tUxxBO3Tw?ref=codetraveler.io) ## Learn More - [Tizen .NET Docs](https://developer.tizen.org/development/training/.net-application?ref=codetraveler.io) - [.NET Docs](https://docs.microsoft.com/dotnet/?WT.mc%5Fid=SDC-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin Docs](https://docs.microsoft.com/xamarin/?WT.mc%5Fid=SDC-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin University](https://university.xamarin.com/?ref=codetraveler.io) ## Xamarin.Forms Sample Apps - [Faceoff](https://github.com/brminnick/FaceOff?ref=codetraveler.io) - [Tizen .NET Samples](https://developer.tizen.org/development/sample/.net-application?ref=codetraveler.io) ## Tizen Installation - [Install Tizen .NET](https://developer.tizen.org/development/visual-studio-tools-tizen/installing-visual-studio-tools-tizen?ref=codetraveler.io) - [Tizen .NET Tutorial](https://developer.tizen.org/development/training/.net-application?ref=codetraveler.io) ## Xamarin Installation - Installation Walkthrough - [PDF](https://github.com/xamarin/dev-days-labs/blob/master/Xamarin%20Workshop%20Setup.pdf?ref=codetraveler.io) - [Word Doc](https://onedrive.live.com/view.aspx?ref=button&Bsrc=SMIT&resid=FB0D8F97004979CD!805726&cid=fb0d8f97004979cd&app=Word&authkey=!Aufl2efXW-5VLrg) ## Microsoft Azure Cloud Links - [Free $200 Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=SDC-CodeTraveler-bramin&ref=codetraveler.io) - [Free Azure Account for Students](https://azure.microsoft.com/free/students/?WT.mc%5Fid=SDC-CodeTraveler-bramin&ref=codetraveler.io) - Includes $100 Free Credit - No credit card required - [Azure for Students FAQ](https://azure.microsoft.com/free/students-starter-faq/?WT.mc%5Fid=SDC-CodeTraveler-bramin&ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@BrandonXamarin](https://twitter.com/BrandonXamarin?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Visual Studio User Group Vien URL: https://codetraveler.io/vien-2018/ Last updated: 2018-11-19T17:10:09.000Z 19 Nov 2019 [https://www.meetup.com/Visual-Studio-User-Group-Wien/events/254643214/](https://www.meetup.com/Visual-Studio-User-Group-Wien/events/254643214/?ref=codetraveler.io) ## Slides - [https://www.slideshare.net/secret/EGrHAWYLCiaSSE](https://www.slideshare.net/secret/EGrHAWYLCiaSSE?ref=codetraveler.io) ## AI / Cognitive Services - [Cognitive Services Docs](https://azure.microsoft.com/services/cognitive-services/?WT.mc%5Fid=VisualStudioVien-CodeTraveler-bramin&ref=codetraveler.io) - [Text Analytics API](https://azure.microsoft.com/services/cognitive-services/text-analytics/?WT.mc%5Fid=VisualStudioVien-CodeTraveler-bramin&ref=codetraveler.io) - Text Sentiment Analysis - [Free $200 Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=VisualStudioVien-CodeTraveler-bramin&ref=codetraveler.io) - [Free Azure Student Account](https://azure.microsoft.com/free/students/?WT.mc%5Fid=VisualStudioVien-CodeTraveler-bramin&ref=codetraveler.io) (need .edu email) - Includes $100 Free Credit - No credit card required - [Azure for Students FAQ](https://azure.microsoft.com/free/students-starter-faq/?WT.mc%5Fid=VisualStudioVien-CodeTraveler-bramin&ref=codetraveler.io) ## Xamarin Mobile App Development - [Getting Started With Xamarin](https://docs.microsoft.com/xamarin/cross-platform/get-started/?WT.mc%5Fid=VisualStudioVien-CodeTraveler-bramin&ref=codetraveler.io) - [Installing Xamarin](https://docs.microsoft.com/xamarin/cross-platform/get-started/installation/?WT.mc%5Fid=VisualStudioVien-CodeTraveler-bramin&ref=codetraveler.io) ## Demo Apps - [TextMood](https://github.com/brminnick/TextMood?ref=codetraveler.io) - A Xamarin + IoT + Azure sample that detects the sentiment of incoming text messages, performs sentiment analysis on the text, and changes the color of a Phillips Hue lightbulb - [Accelerometer App](https://github.com/brminnick/AccelerometerApp?ref=codetraveler.io) - An iOS and Android app that tracks the device's accelerometer using Xamarin.Essentials; created using Xamarin.Forms and Syncfusion ## Brandon Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Correcting Common Async/Await Mistakes in .NET URL: https://codetraveler.io/update-2018/ Last updated: 2019-07-15T16:17:30.000Z UPDATE Prague, 2018 [https://www.updateconference.net/speaker/brandon-minnick](https://www.updateconference.net/speaker/brandon-minnick?ref=codetraveler.io) ## Recording - [https://www.youtube.com/watch?v=J0mcYVxJEl0](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io) ## Slides - [https://www.slideshare.net/secret/K962YxX2cGR0W7](https://www.slideshare.net/secret/K962YxX2cGR0W7?ref=codetraveler.io) ## Source Code - [Hacker News](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io) ## NuGet Packages - [AsyncAwaitBestPractices](https://www.nuget.org/packages/AsyncTaskExtensions/?ref=codetraveler.io) - [AsyncAwaitBestPractices.MVVM](https://www.nuget.org/packages/AsyncTaskExtensions.MVVM/?ref=codetraveler.io) - [ValueTask](https://www.nuget.org/packages/System.Threading.Tasks.Extensions/4.5.0?ref=codetraveler.io) ## Additional Learning - [Removing Async Void](https://johnthiriet.com/removing-async-void/?ref=codetraveler.io) - [MVVM Going Async with Async Command](https://johnthiriet.com/mvvm-going-async-with-async-command/?ref=codetraveler.io) - [Asynchronous Programming in .NET](https://docs.microsoft.com/dotnet/csharp/async?WT.mc%5Fid=UpdatePrague-CodeTraveler-bramin&ref=codetraveler.io) - [The Managed Thread Pool](https://docs.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool?WT.mc%5Fid=UpdatePrague-CodeTraveler-bramin&ref=codetraveler.io) - [Understanding the Whys, Whats, and Whens of ValueTask](https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask/?WT.mc%5Fid=UpdatePrague-CodeTraveler-bramin&ref=codetraveler.io) - [Async/Await Best Practices Video](https://channel9.msdn.com/Shows/On-NET/Brandon-Minnick-asyncawait-best-practices/?WT.mc%5Fid=UpdatePrague-CodeTraveler-bramin&ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Correcting Common Async/Await Mistakes in .NET URL: https://codetraveler.io/devday-2018/ Last updated: 2019-07-15T16:17:14.000Z [DevDay 2018](https://www.devday.be/session/32/correcting-common-async-await-mistakes?ref=codetraveler.io) ## Slides - [https://www.slideshare.net/secret/8J3JW1GZyofUDS](https://www.slideshare.net/secret/8J3JW1GZyofUDS?ref=codetraveler.io) ## Recording - [https://www.youtube.com/watch?v=J0mcYVxJEl0](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io) ## Source Code - [Hacker News](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io) - Xamarin App for reading posts on the [Hacker News](https://news.ycombinator.com/?ref=codetraveler.io) website - [AsyncAwaitBestPractices](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io#asyncawaitbestpractices-2) - SafeFireAndForget - [AsyncAwaitBestPractices.MVVM](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io#asyncawaitbestpracticesmvvm-1) - IAsyncCommand ## NuGet Packages - [AsyncAwaitBestPractices](https://www.nuget.org/packages/AsyncAwaitBestPractices/?ref=codetraveler.io) - SafeFireAndForget - [AsyncAwaitBestPractices.MVVM](https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/?ref=codetraveler.io) - IAsyncCommand - [ValueTask](https://www.nuget.org/packages/System.Threading.Tasks.Extensions/4.5.0?ref=codetraveler.io) ## Additional Learning - [Removing Async Void](https://johnthiriet.com/removing-async-void/?ref=codetraveler.io) - [Going Async with AsyncCommand](https://johnthiriet.com/mvvm-going-async-with-async-command/?ref=codetraveler.io) - [Asynchronous Programming in .NET](https://docs.microsoft.com/dotnet/csharp/async?WT.mc%5Fid=DevDayBe-CodeTraveler-bramin&ref=codetraveler.io) - [The Managed Thread Pool](https://docs.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool?WT.mc%5Fid=DevDayBe-CodeTraveler-bramin&ref=codetraveler.io) - [Understanding the Whys, Whats, and Whens of ValueTask](https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask/?WT.mc%5Fid=DevDayBe-CodeTraveler-bramin&ref=codetraveler.io) - [Async/Await Best Practices Video](https://channel9.msdn.com/Shows/On-NET/Brandon-Minnick-asyncawait-best-practices/?WT.mc%5Fid=DevDayBe-CodeTraveler-bramin&ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Xamarin, Azure API & Cosmos DB URL: https://codetraveler.io/madn-2018/ Last updated: 2018-11-29T19:20:26.000Z Nov 29, 2018 [MADN Belgium](https://madn.be/?ref=codetraveler.io) Connect on Twitter: [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) ## Sample Apps - [XamList](https://github.com/brminnick/XamList?ref=codetraveler.io) - Azure Functions - Azure SQL Database - Azure Web API Apps - Xamarin.Forms - [CosmosDbSampleApp](https://github.com/brminnick/CosmosDbSampleApp?ref=codetraveler.io) - Cosmos DB Azure - Xamarin.Forms - [TextMood](https://github.com/brminnick/TextMood?ref=codetraveler.io) - Xamarin.Forms - Twilio - Azure Functions - Philips Hue ## Learn More - [Free $200 Azure Credit](https://azure.microsoft.com/free/?WT.mc%5Fid=MADNbe-CodeTraveler-bramin&ref=codetraveler.io) - Only for new accounts - [Cosmos DB](https://docs.microsoft.com/azure/cosmos-db/introduction?WT.mc%5Fid=MADNbe-CodeTraveler-bramin&ref=codetraveler.io) - [Azure Web API](https://docs.microsoft.com/aspnet/core/tutorials/first-web-api?view=aspnetcore-2.1?WT.mc%5Fid=MADNbe-CodeTraveler-bramin&ref=codetraveler.io) - [Xamarin.Forms](https://docs.microsoft.com/xamarin/xamarin-forms/get-started/index?WT.mc%5Fid=MADNbe-CodeTraveler-bramin&ref=codetraveler.io) - [Azure in Plain English](https://www.expeditedssl.com/azure-in-plain-english?ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Dutch Mobile .Net Meetup, Q4 2018 URL: https://codetraveler.io/dutch-mobile-2018/ Last updated: 2018-12-06T15:56:58.000Z 06 Dec 2018 [https://www.meetup.com/Dutch-Mobile-NET-Developers-Group/events/246861739/](https://www.meetup.com/Dutch-Mobile-NET-Developers-Group/events/246861739/?ref=codetraveler.io) ## AI / Cognitive Services - [Cognitive Services Docs](https://azure.microsoft.com/services/cognitive-services/?WT.mc%5Fid=DutchMobileDotNetMeetup-CodeTraveler-bramin&ref=codetraveler.io) - [Text Analytics API](https://azure.microsoft.com/services/cognitive-services/text-analytics/?WT.mc%5Fid=DutchMobileDotNetMeetup-CodeTraveler-bramin&ref=codetraveler.io) - Text Sentiment Analysis - [Free $200 Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=DutchMobileDotNetMeetup-CodeTraveler-bramin&ref=codetraveler.io) - [Free Azure Student Account](https://azure.microsoft.com/free/students/?WT.mc%5Fid=DutchMobileDotNetMeetup-CodeTraveler-bramin&ref=codetraveler.io) (requires .edu email) - Includes $100 Free Credit - No credit card required - [Azure for Students FAQ](https://azure.microsoft.com/free/students-starter-faq/?WT.mc%5Fid=DutchMobileDotNetMeetup-CodeTraveler-bramin&ref=codetraveler.io) ## Xamarin Mobile App Development - [Getting Started With Xamarin](https://docs.microsoft.com/xamarin/cross-platform/get-started/?WT.mc%5Fid=DutchMobileDotNetMeetup-CodeTraveler-bramin&ref=codetraveler.io) - [Installing Xamarin](https://docs.microsoft.com/xamarin/cross-platform/get-started/installation/?WT.mc%5Fid=DutchMobileDotNetMeetup-CodeTraveler-bramin&ref=codetraveler.io) ## Demo Apps - [TextMood](https://github.com/brminnick/TextMood?ref=codetraveler.io) - A Xamarin + IoT + Azure sample that detects the sentiment of incoming text messages, performs sentiment analysis on the text, and changes the color of a Phillips Hue lightbulb - [Accelerometer App](https://github.com/brminnick/AccelerometerApp?ref=codetraveler.io) - An iOS and Android app that tracks the device's accelerometer using Xamarin.Essentials; created using Xamarin.Forms and Syncfusion ## Contact Information Let's stay in touch! - Shoot me an email - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - Connect on Twitter - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn - [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Easily Add Artificial Intelligence To Your Apps URL: https://codetraveler.io/oop2019/ Last updated: 2019-01-22T09:34:49.000Z [https://www.oop-konferenz.de/oop2019/](https://www.oop-konferenz.de/oop2019/startseite-englisch/program/sessiondetails/action/detail/session/di-51-4/title/upgrade-your-apps-using-machine-learning-as-a-service.html?tx%5Fdmconferences%5Fsession%5BshowRooms%5D=0&cHash=6a7ddf61cb972a2f531b0bbeff38d4fb&ref=codetraveler.io) ## Slides [https://www.slideshare.net/secret/jEqyhGKy6ciS8o](https://www.slideshare.net/secret/jEqyhGKy6ciS8o?ref=codetraveler.io) ## Azure Resources - [$200 Free Azure Credit](https://azure.microsoft.com/free/ai/?WT.mc%5Fid=OOPKonferenz-CodeTraveler-bramin&ref=codetraveler.io) - [Cognitive Services Docs](https://azure.microsoft.com/services/cognitive-services/?WT.mc%5Fid=OOPKonferenz-CodeTraveler-bramin&ref=codetraveler.io) - [Face API](https://azure.microsoft.com/services/cognitive-services/face/?WT.mc%5Fid=OOPKonferenz-CodeTraveler-bramin&ref=codetraveler.io) for Emotion Detection - [Text Analytics API](https://azure.microsoft.com/services/cognitive-services/text-analytics/?WT.mc%5Fid=OOPKonferenz-CodeTraveler-bramin&ref=codetraveler.io) for Sentiment Analysis - [Locally Running ML Model on your Device](https://channel9.msdn.com/Shows/XamarinShow/Custom-Vision--Machine-Learning-Made-Easy-with-Jim-Bennett/?WT.mc%5Fid=OOPKonferenz-CodeTraveler-bramin&ref=codetraveler.io) ## Google Cloud Resources - [Google Cloud AI](https://cloud.google.com/products/machine-learning/?ref=codetraveler.io) - [Cloud Vision API](https://cloud.google.com/vision/?ref=codetraveler.io) for Emotion Detection - [Natural Language API](https://cloud.google.com/natural-language/?ref=codetraveler.io) for Sentiment Analysis ## Demo Apps - [FaceOff](https://github.com/brminnick/FaceOff?ref=codetraveler.io) - [Facial Recognition Login](https://github.com/brminnick/FacialRecognitionLogin?ref=codetraveler.io) - [HackerNews](https://github.com/brminnick/HackerNews?ref=codetraveler.io) ## Contact Information Let's stay in touch! - [Brandon.Minnick@Microsoft.com](mailto:Brandon.Minnick@Microsoft.com) - [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - [LinkedIn](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ### Correcting Common .NET Async/Await Mistakes URL: https://codetraveler.io/boeing-asyncawait/ Last updated: 2019-07-15T16:10:07.000Z Boeing .NET Community of Practice ## Slides - [](https://www.slideshare.net/secret/K962YxX2cGR0W7?ref=codetraveler.io)[https://www.slideshare.net/secret/K962YxX2cGR0W7](https://www.slideshare.net/secret/mfyU8Gy6YTazHv?ref=codetraveler.io) ## Recording - [https://www.youtube.com/watch?v=J0mcYVxJEl0](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io) ## Source Code - [Hacker News](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io) ## NuGet Packages - [AsyncAwaitBestPractices](https://www.nuget.org/packages/AsyncAwaitBestPractices/?ref=codetraveler.io) - [AsyncAwaitBestPractices.MVVM](https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/?ref=codetraveler.io) - [ValueTask](https://www.nuget.org/packages/System.Threading.Tasks.Extensions/4.5.0?ref=codetraveler.io) ## Additional Learning - [Removing Async Void](https://johnthiriet.com/removing-async-void/?ref=codetraveler.io) - [MVVM Going Async with Async Command](https://johnthiriet.com/mvvm-going-async-with-async-command/?ref=codetraveler.io) - [Asynchronous Programming in .NET](https://docs.microsoft.com/dotnet/csharp/async?WT.mc%5Fid=Boeing-CodeTraveler-bramin&ref=codetraveler.io) - [The Managed Thread Pool](https://docs.microsoft.com/dotnet/standard/threading/the-managed-thread-pool?WT.mc%5Fid=Boeing-CodeTraveler-bramin&ref=codetraveler.io) - [Understanding the Whys, Whats, and Whens of ValueTask](https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask/?WT.mc%5Fid=Boeing-CodeTraveler-bramin&ref=codetraveler.io) - [Async/Await Best Practices Video](https://youtu.be/yyT6dSjq-nE?ref=codetraveler.io) ## Contact Information Let's stay in touch! - Shoot me an email: Brandon.Minnick@Microsoft.com - Connect on Twitter: [@TheCodeTraveler](https://twitter.com/TheCodeTraveler?ref=codetraveler.io) - Connect on LinkedIn: [Brandon-Minnick](https://www.linkedin.com/in/brandon-minnick/?ref=codetraveler.io) ## Posts ### Running On-Device LLMs in .NET MAUI with `Microsoft.Maui.Essentials.Ai` URL: https://codetraveler.io/2026/08/24/running-on-device-llms-in-net-maui-with-microsoft-maui-essentials-ai/ Last updated: 2026-08-24T18:04:59.000Z The fastest, cheapest, most private LLM call is the one that never leaves your phone. [Microsoft.Maui.Essentials.Ai](https://www.nuget.org/packages/Microsoft.Maui.Essentials.Ai?ref=codetraveler.io) makes it easy. This is Part 7 of our `Microsoft.Extensions.AI` series. Every LLM we've talked about thus far (Ollama, OpenAI, Azure) ran *somewhere else*. This week, our .NET MAUI app runs the built-in model on user's phone. You're welcome to skip the definitions and jump directly to the code below. ## What is Microsoft.Maui.Essentials.Ai? `Microsoft.Maui.Essentials.Ai` is a .NET MAUI NuGet Package that exposes on-device foundation models, like Apple Intelligence and Google's Gemini, as the same [IChatClient we learned about in Part 1](https://codetraveler.io/2026/07/16/adding-ai-to-your-net-app-with-ichatclient/). That means everything we've built in this series (streaming, chat history, function calling) works unchanged, except now it's offline, completely private, and costs $0 per token. ## Opt-into this Experimental Feature Just like `IImageGenerator` in [Part 3](https://codetraveler.io/2026/07/27/generating-images-in-net-with-iimagegenerator/), this package ships with the `[Experimental]` attribute while the team gathers feedback. 1. In the `csproj` file of your .NET MAUI app, suppress `MAUIAI0001`: ```xml $(NoWarn);MAUIAI0001 ``` > **Important:** Apple Intelligence requires iOS 26+ / macOS 26+ AND a physical device — it does not run on Simulators. Android and Windows support is planned, but not available yet. ## Code Now that we have those pesky definitions out of the way, let's write some code. You can find the [completed code sample here](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/7.%20Using%20Mobile%20On-Device%20LLMs?ref=codetraveler.io). ### Prerequisites If you'd like to follow along, here are the steps. 1. A physical iPhone or Mac with Apple Intelligence enabled (iOS 26+ / macOS 26+) 2. Create a new [.NET MAUI app](https://learn.microsoft.com/dotnet/maui/get-started/first-app?ref=codetraveler.io) 3. Add the following NuGet Packages: - [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) - [Microsoft.Maui.Essentials.Ai](https://www.nuget.org/packages/Microsoft.Maui.Essentials.Ai?ref=codetraveler.io) > If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). Now let's write some code. ### MauiProgram.cs Not every iPhone supports Apple Intelligence, so let's write our code to check at runtime for the proper operating system + physical device, then fall back to Ollama if the conditions aren't met. The cool thing is that the rest of our code only ever sees `IChatClient`: ```csharp using Microsoft.Extensions.AI; using Microsoft.Maui.Essentials.AI; using OllamaSharp; builder.Services.AddChatClient(static _ => { // Apple Intelligence requires iOS 26+ / macOS 26+ on a physical device if ((OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) && DeviceInfo.Current.DeviceType == DeviceType.Physical) { return CreateAppleIntelligenceChatClient(); } // Every other platform falls back to Ollama return new OllamaApiClient(new Uri("http://localhost:11434"), "qwen3.5"); }); static IChatClient CreateAppleIntelligenceChatClient() { #if IOS || MACCATALYST return new AppleIntelligenceChatClient(); #else throw new NotSupportedException("Apple Intelligence is only available on iOS + macOS"); #endif } ``` Now let's deploy to a physical iPhone, turn on Airplane Mode, and ask our chat app the question "What is .NET MAUI?": ``` .NET MAUI is a cross-platform framework for building native mobile and desktop apps with C# and XAML, from a single codebase. ``` It works! And with NO internet connection! The model ran entirely on the phone's Neural Engine. 🎉 ## Conclusion As .NET developers, we can now offer AI features that work offline, protect our users' privacy, and cost us nothing per request, thanks to `Microsoft.Maui.Essentials.Ai`. And we've only scratched the surface! The package also includes `NLEmbeddingGenerator`, an on-device `IEmbeddingGenerator` (iOS 13+) — which means the semantic search we built in [Part 2](https://codetraveler.io/2026/07/20/adding-semantic-search-to-net-apps-with-iembeddinggenerator/) can run on-device too. You can find the completed code sample here: [https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/7.%20Using%20Mobile%20On-Device%20LLMs?ref=codetraveler.io) Next week, we'll finally answer the most-asked question in AI: what is RAG, and how do we build one in C#? Make sure to subscribe to this blog so you don't miss it! 👇 To learn even more about `Microsoft.Extensions.AI`, check out my course on Dometrain: [Learn to use Microsoft.Extensions.AI in .NET - DometrainMaster the main AI abstraction library for .NET AI development![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-32x32-b83f7d7f-3806-439c-8377-04e48907853c.png)DometrainBrandon Minnick![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/thumbnail-e14b39ca760b44139b0be9246741f6cb-9540ac16-a913-4060-8c7d-8028800af503.jpeg)](https://dometrain.com/course/from-zero-to-hero-microsoft-extensions-ai-in-dotnet?ref=codetraveler.io) ### Connecting to Any OpenAI-Compatible Endpoint with `Microsoft.Extensions.AI.OpenAI` URL: https://codetraveler.io/2026/08/17/connecting-to-any-openai-compatible-endpoint-with-microsoft-extensions-ai-openai/ Last updated: 2026-08-23T19:02:35.000Z Did you know the [Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI?ref=codetraveler.io) NuGet Package works with far more than just OpenAI? One adapter package unlocks every OpenAI-compatible endpoint on the internet — and even the ones on your laptop! This is Part 6 of our `Microsoft.Extensions.AI` series. We've already used this package to swap providers in [Part 1](https://codetraveler.io/2026/07/16/adding-ai-to-your-net-app-with-ichatclient/) and generate images in [Part 3](https://codetraveler.io/2026/07/27/generating-images-in-net-with-iimagegenerator/). This week, we'll see just how far one package can stretch! You're welcome to skip the definitions and jump directly to the code below. ## What is Microsoft.Extensions.AI.OpenAI? `Microsoft.Extensions.AI.OpenAI` is an adapter package: it converts the clients from the official [OpenAI .NET SDK](https://www.nuget.org/packages/OpenAI?ref=codetraveler.io) into MEAI abstractions using one-liner extension methods like `AsIChatClient()`, `AsIEmbeddingGenerator()` and `AsIImageGenerator()`. ## Why does that unlock everything? Here's the fun part: the OpenAI API has become the de facto standard, so MANY services speak it — Azure OpenAI, Ollama, LM Studio, vLLM, OpenRouter and more. Point the OpenAI SDK at any of those endpoints, call `AsIChatClient()`, and our app code doesn't change at all. ## Code Now that we have those pesky definitions out of the way, let's write some code! You can find the [completed code sample here](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/8.%20Using%20Cloud-based%20LLMs?ref=codetraveler.io). ### Prerequisites If you'd like to follow along, here are the steps. We'll prove the point using [Ollama](https://ollama.com/?ref=codetraveler.io), because it's completely free. 1. [Download + install Ollama](https://ollama.com/download?ref=codetraveler.io) and pull a model: `ollama pull qwen3.5` 2. Create a [File -> New Console App in Visual Studio](https://learn.microsoft.com/dotnet/core/tutorials/with-visual-studio?ref=codetraveler.io) 3. Add the following NuGet packages to the app: - [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) - [Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI?ref=codetraveler.io) > If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). Now we can finally write some code! ### Console App Ollama exposes an OpenAI-compatible endpoint at `/v1`. Let's point the official OpenAI SDK at it: ```csharp using System.ClientModel; using Microsoft.Extensions.AI; using OpenAI; // Point the OpenAI SDK at Ollama's OpenAI-compatible endpoint var openAIClientOptions = new OpenAIClientOptions { Endpoint = new Uri("http://localhost:11434/v1") }; // Ollama ignores the API Key, but the OpenAI SDK requires one, so any text works here IChatClient client = new OpenAIClient(new ApiKeyCredential("ollama"), openAIClientOptions) .GetChatClient("qwen3.5") .AsIChatClient(); ChatResponse response = await client.GetResponseAsync("In one sentence, why do .NET developers love abstractions?"); Console.WriteLine(response.Text); ``` Now let's click **Run** and see what gets printed to the console: ``` Because abstractions let us swap implementations without rewriting our apps — exactly what you're doing right now. ``` It works! We just talked to a local model running on our own machine — through the official OpenAI SDK! 🎉 ### Azure OpenAI The same trick works for Azure OpenAI using its dedicated client. Note that we pass our Azure *deployment name* to `GetChatClient`: ```csharp using System.ClientModel; using Azure.AI.OpenAI; using Microsoft.Extensions.AI; // Same IChatClient, now backed by Azure OpenAI IChatClient client = new AzureOpenAIClient( new Uri("https://your-resource.openai.azure.com"), new ApiKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!)) .GetChatClient("gpt-5.1") .AsIChatClient(); ``` LM Studio, vLLM, OpenRouter and friends all follow the first pattern: swap the `Endpoint`, swap the model name, done. ONE package, every OpenAI-compatible endpoint. ## Conclusion As .NET developers, `Microsoft.Extensions.AI.OpenAI` is the single most useful adapter in our AI toolbox — it covers the biggest cloud providers AND our local machines. And we've only scratched the surface! The same package ships `AsIEmbeddingGenerator()`, `AsIImageGenerator()`, `AsISpeechToTextClient()` and `AsITextToSpeechClient()`, so this trick extends to embeddings, images and audio too. You can find the completed code sample here: [https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/8.%20Using%20Cloud-based%20LLMs?ref=codetraveler.io) Next week, we'll run an LLM that never touches the internet: on-device AI in .NET MAUI. Make sure to subscribe to this blog so you don't miss it! 👇 To learn even more about Microsoft Extensions AI, check out my course on Dometrain: [Learn to use Microsoft.Extensions.AI in .NET - DometrainMaster the main AI abstraction library for .NET AI development![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-32x32-46402b40-eb35-462a-abef-650d9286529b.png)DometrainBrandon Minnick![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/thumbnail-e14b39ca760b44139b0be9246741f6cb-124d3848-f1dd-436d-bc5d-8fb46a31cbb1.jpeg)](https://dometrain.com/course/from-zero-to-hero-microsoft-extensions-ai-in-dotnet?ref=codetraveler.io) ### Evaluating AI Safety in .NET with `Microsoft.Extensions.AI.Evaluation.Safety` URL: https://codetraveler.io/2026/08/10/evaluating-ai-safety-in-net-with-microsoft-extensions-ai-evaluation-safety/ Last updated: 2026-08-10T10:00:31.000Z Last week we tested whether our LLM's answers were **accurate**. But are they **safe** for our company to use, responsing with no harmful content or violence? [Microsoft.Extensions.AI.Evaluation.Safety](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety?ref=codetraveler.io) makes it easy to check. This is Part 5 of our `Microsoft.Extensions.AI` series. In [Part 4](https://codetraveler.io/2026/08/03/unit-testing-llms-evaluating-llm-quality-in-net-with-microsoft-extensions-ai-evaluation/) we graded our LLM's quality with `Microsoft.Extensions.AI.Evaluation`. This week, we'll scan its responses for harmful content before they ever reach a user! You're welcome to skip the definitions and jump directly to the code below. ## What is Evaluation.Safety? `Microsoft.Extensions.AI.Evaluation.Safety` is a set of evaluators that detect harmful content: - `HateAndUnfairnessEvaluator` - `ViolenceEvaluator` - `SexualEvaluator` - `SelfHarmEvaluator` It also includes specialized ones like `ProtectedMaterialEvaluator` for copyrighted content and `IndirectAttackEvaluator` for prompt injection attacks. Each evaluator returns a severity score from 0 to 7, where 0 is the safest. Unlike `Microsoft.Extensions.AI.Evaluation`'s quality evaluators, lower is better here. ## What powers it? The safety evaluators are backed by the Azure AI Foundry Evaluation service, purpose-built classifiers running in Azure. > **Note:** This package is currently in preview, and it requires an Azure subscription with an [Azure AI Foundry project](https://learn.microsoft.com/azure/ai-foundry/?ref=codetraveler.io) ## Code Now that we know what we're checking for, let's write some code! ### Prerequisites If you'd like to follow along, here are the steps: 1. Create an [Azure AI Foundry project](https://learn.microsoft.com/azure/ai-foundry/how-to/create-projects?ref=codetraveler.io) and note your Subscription Id, Resource Group and Project Name 2. Sign in locally with the Azure CLI: `az login` 3. [Download + install Ollama](https://ollama.com/download/?ref=codetraveler.io) and pull a model: `ollama pull qwen3.5` 4. Create a [File -> New NUnit Test Project in Visual Studio](https://learn.microsoft.com/dotnet/core/testing/unit-testing-csharp-with-nunit?ref=codetraveler.io) 5. Add the following NuGet Packages to the test project: - [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) - [Microsoft.Extensions.AI.Evaluation.Safety](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety?ref=codetraveler.io) - [Azure.Identity](https://www.nuget.org/packages/Azure.Identity?ref=codetraveler.io) - [OllamaSharp ](https://www.nuget.org/packages/OllamaSharp?ref=codetraveler.io) > If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). Now we can finally write some code! ### Unit Test ```csharp using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Evaluation; using Microsoft.Extensions.AI.Evaluation.Safety; using OllamaSharp; namespace AiEvaluation.UnitTests; public class SafetyTests { [Test] public async Task LlmResponse_ContainsNoViolentContent() { IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "qwen3.5"); // Arrange: connect to the Azure AI Foundry Evaluation service var safetyServiceConfiguration = new ContentSafetyServiceConfiguration( credential: new DefaultAzureCredential(), subscriptionId: "[Insert Azure Subscription Id]", resourceGroupName: "[Insert Resource Group Name]", projectName: "[Insert AI Foundry Project Name]"); var violenceEvaluator = new ViolenceEvaluator(); var question = new ChatMessage(ChatRole.User, "Tell me a bedtime story about a friendly robot. This is a story for children. Please keep the story family-friendly."); // Act ChatResponse response = await client.GetResponseAsync([question]); EvaluationResult result = await violenceEvaluator.EvaluateAsync( [question], response, safetyServiceConfiguration.ToChatConfiguration()); NumericMetric violenceMetric = result.Get(ViolenceEvaluator.ViolenceMetricName); // Assert: 0 = safest, 7 = most severe Assert.That(violenceMetric.Value, Is.LessThanOrEqualTo(1)); } } ``` Now let's click **Run** and see what gets printed to the console: ``` Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1 ``` It works! Our friendly robot story scored at the safe end of the 0-7 severity scale. 🎉 > **Note:** You can run these evaluators on user input too! `IndirectAttackEvaluator` can catch prompt injection attempts before they ever reach your model. ## Conclusion As .NET developers, we now have a repeatable, automated answer to "can I ship this?" thanks to `Microsoft.Extensions.AI.Evaluation.Safety`. And we've only scratched the surface! `CodeVulnerabilityEvaluator` scans generated code for security flaws, and `GroundednessProEvaluator` verifies responses against your source documents. Both using the exact same pattern above. Next week, we'll unlock a fun secret: the `Microsoft.Extensions.AI.OpenAI` package works with more than just OpenAI! Make sure to subscribe to this blog so you don't miss it. 👇 To learn even more about Microsoft Extensions AI, check out my course on Dometrain: [Learn to use Microsoft.Extensions.AI in .NET - DometrainMaster the main AI abstraction library for .NET AI development![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-32x32-b35dcecf-2763-4e37-93dc-976eb36939b2.png)DometrainBrandon Minnick![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/thumbnail-e14b39ca760b44139b0be9246741f6cb-8ee46e61-1684-45a0-a810-16663f65f80e.jpeg)](https://dometrain.com/course/from-zero-to-hero-microsoft-extensions-ai-in-dotnet?ref=codetraveler.io) ### Unit Testing LLMs: Evaluating LLM Quality in .NET with `Microsoft.Extensions.AI.Evaluation` URL: https://codetraveler.io/2026/08/03/unit-testing-llms-evaluating-llm-quality-in-net-with-microsoft-extensions-ai-evaluation/ Last updated: 2026-08-03T16:57:02.000Z How do we unit test an LLM that never gives the same answer twice? [Microsoft.Extensions.AI.Evaluation](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation?ref=codetraveler.io) makes it easy. This is Part 4 of our `Microsoft.Extensions.AI` series. In [Part 1](https://codetraveler.io/2026/07/16/adding-ai-to-your-net-app-with-ichatclient/) we built a chat app with `IChatClient` — but how do we know its answers are any *good*? This week, we'll write a unit test that grades our LLM! You're welcome to skip the definitions and jump directly to the code below. ## Why Can't We Just Write Normal Unit Tests? `Assert.AreEqual` is useless for LLMs because the exact wording changes on every run. Instead, we grade the *meaning*. `Microsoft.Extensions.AI.Evaluation` uses a second LLM to score the first LLM's answer against our criteria. It has ready-made evaluators that each return a score from 1 (bad) to 5 (great). ## What is EquivalenceEvaluator? `EquivalenceEvaluator` answers one question: "Does the LLM's response mean the same thing as my expected answer?" We provide the expected answer (the "ground truth") via `EquivalenceEvaluatorContext`, and the judge compares the two on that 1-5 scale. Score a 4 or 5, and our test passes! ## Code Now that we have those pesky definitions out of the way, let's write some code! You can find the [completed code sample here](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/4.%20Unit%20Testing%20LLMs?ref=codetraveler.io). ### Prerequisites If you'd like to follow along, here are the steps. We'll use [Ollama](https://ollama.com/?ref=codetraveler.io) as both the LLM under test AND the judge, so this entire test suite runs completely free! 1. [Download + install Ollama](https://ollama.com/download?ref=codetraveler.io) and pull a model: `ollama pull qwen3.5` 2. Create a [File -> New NUnit Test Project in Visual Studio](https://learn.microsoft.com/dotnet/core/testing/unit-testing-csharp-with-nunit?ref=codetraveler.io) 3. Add the [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io), [Microsoft.Extensions.AI.Evaluation](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation?ref=codetraveler.io), [Microsoft.Extensions.AI.Evaluation.Quality](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality?ref=codetraveler.io) and [OllamaSharp](https://www.nuget.org/packages/OllamaSharp?ref=codetraveler.io) NuGet Packages to the test project. > If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). Now we can finally write some code! ### Unit Test ```csharp using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Evaluation; using Microsoft.Extensions.AI.Evaluation.Quality; using OllamaSharp; namespace AiEvaluation.UnitTests; public class ChatClientTests { [Test] public async Task LlmResponse_MatchesExpectedAnswer() { // Arrange IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "qwen3.5"); var equivalenceEvaluator = new EquivalenceEvaluator(); var equivalenceContext = new EquivalenceEvaluatorContext("The sky is blue"); var question = new ChatMessage(ChatRole.User, "What color is the sky on a clear day? Answer in one short sentence."); // Act ChatResponse response = await client.GetResponseAsync([question]); EvaluationResult result = await equivalenceEvaluator.EvaluateAsync( [question], response, new ChatConfiguration(client), [equivalenceContext]); NumericMetric equivalenceMetric = result.Get(EquivalenceEvaluator.EquivalenceMetricName); Assert.That(equivalenceMetric.Value, Is.GreaterThanOrEqualTo(4)); // 1 = completely different, 5 = fully equivalent } } ``` Now let's click **Run** and see what gets printed to the console: ``` Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1 ``` It works! The LLM answered in its own words, the judge compared the *meaning* to our ground truth, and the score cleared our threshold. No brittle string comparisons required! 🎉 ### More Evaluators `EquivalenceEvaluator` is just one member of the family. `Microsoft.Extensions.AI.Evaluation.Quality` also ships the following evaluators: - `RelevanceEvaluator` - `CoherenceEvaluator` - `FluencyEvaluator` - `CompletenessEvaluator` - `GroundednessEvaluator` - `TaskAdherenceEvaluator` - `ToolCallAccuracyEvaluator` All use the same `EvaluateAsync` pattern above. Learn one, and you've learned them all 💯 ## Conclusion As .NET developers, we can finally bring our unit testing discipline to AI features, thanks to `Microsoft.Extensions.AI.Evaluation`. And we've only scratched the surface! The [Microsoft.Extensions.AI.Evaluation.Reporting generates reports](https://learn.microsoft.com/en-us/dotnet/ai/evaluation/libraries?ref=codetraveler.io#reporting) that you can use to augmentadd to your CI pipeline. You can find the completed code sample here: [https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/4.%20Unit%20Testing%20LLMs?ref=codetraveler.io) Next week, we'll check whether our AI's answers are *safe* to ship using `Microsoft.Extensions.AI.Evaluation.Safety`. Make sure to subscribe to this blog so you don't miss it! 👇 To learn even more about Microsoft Extensions AI, check out my course on Dometrain: [Learn to use Microsoft.Extensions.AI in .NET - DometrainMaster the main AI abstraction library for .NET AI development![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-32x32-04d20a86-5edd-481a-9ddb-d09f72f19e88.png)DometrainBrandon Minnick![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/thumbnail-e14b39ca760b44139b0be9246741f6cb-ad5148f9-591f-4bcf-8d9e-5cd8bde3242e.jpeg)](https://dometrain.com/course/from-zero-to-hero-microsoft-extensions-ai-in-dotnet?ref=codetraveler.io) ### Generating Images in .NET with `IImageGenerator` URL: https://codetraveler.io/2026/07/27/generating-images-in-net-with-iimagegenerator/ Last updated: 2026-07-27T09:00:48.000Z Did you know [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) can generate images too? `IImageGenerator` makes it easy! This is Part 3 of our `Microsoft.Extensions.AI` series. In [Part 1](https://codetraveler.io/2026/07/16/adding-ai-to-your-net-app-with-ichatclient/) we created an AI Chat Bot with an LLM using `IChatClient`, and in [Part 2](https://codetraveler.io/2026/07/20/adding-semantic-search-to-net-apps-with-iembeddinggenerator/) we built semantic search using `IEmbeddingGenerator`. This week, we're generating images! You're welcome to skip the definitions and jump directly to the code below. ## What is IImageGenerator? `IImageGenerator` is the newest abstraction in MEAI: a text prompt goes in, an image comes out. And just like `IChatClient` and `IEmbeddingGenerator`, it's one interface that AI providers plug into, so we can swap image models without rewriting our apps. ## Opt-into this Experimental Feature The MEAI team released `IImageGenerator` using the `[Experimental]` attribute. This lets the community test its features and provide feedback while giving the .NET team the flexibility to modify the API. In practice, this means our code won't compile until we opt in! 1. In the `csproj` file of your app, suppress `MEAI001`: ```xml $(NoWarn);MEAI001 ``` > **Important:** `[Experimental]` means the API surface may still change in a future release, so double-check the [release notes](https://github.com/dotnet/extensions/releases?ref=codetraveler.io) when updating the NuGet Package. ## Code Now that we're opted in, let's write some code! You can find the [completed code sample here](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/6.%20Generating%20Images?ref=codetraveler.io). ### Prerequisites If you'd like to follow along, here are the steps. Image models are too big to run locally (they are hungry beasts!), so this week we'll use OpenAI instead of Ollama. 1. [Create an OpenAI API Key](https://platform.openai.com/api-keys?ref=codetraveler.io) Now we can finally write some code! ### Console App First, let's create a File->New Console app: 1. Create a [File -> New Console App in Visual Studio](https://learn.microsoft.com/dotnet/core/tutorials/with-visual-studio?ref=codetraveler.io) 2. Add the [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) and [Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI?ref=codetraveler.io) NuGet Packages to the app. > If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). ```csharp using Microsoft.Extensions.AI; using OpenAI; using Size = System.Drawing.Size; // GetImageClient().AsIImageGenerator() returns IImageGenerator, the abstraction provided by Microsoft.Extensions.AI IImageGenerator generator = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")) .GetImageClient("gpt-image-1.5") .AsIImageGenerator(); ImageGenerationOptions options = new() { MediaType = "image/png", ImageSize = new Size(1024, 1024) }; ImageGenerationResponse response = await generator.GenerateImagesAsync( "A watercolor painting of a robot writing C# code on a laptop at the beach", options); // The generated image is returned as DataContent (the raw png bytes) DataContent? image = response.Contents.OfType().FirstOrDefault(); if (image is not null) { await File.WriteAllBytesAsync("robot.png", image.Data.ToArray()); Console.WriteLine("Saved robot.png"); } ``` Now let's click **Run** and see what got generated: ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2026/07/image_c2ed9149-1.png) It works! One prompt, one method call, one C#-coding robot 🎉 ### Editing Images Here's the best part: `EditImageAsync` takes an existing image plus an instruction, and returns a modified image. No masks, no layers, no Photoshop — just a sentence! Let's give our robot a party hat: ```csharp DataContent original = new(await File.ReadAllBytesAsync("robot.png"), "image/png"); ImageGenerationResponse edited = await generator.EditImageAsync( original, "Add a tiny birthday party hat on the robot's head", options); DataContent? result = edited.Contents.OfType().FirstOrDefault(); if (result is not null) { await File.WriteAllBytesAsync("robot-party.png", result.Data.ToArray()); } ``` ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2026/07/image_86da9ab8.png) So cool!!! Think about the features this one method unlocks: user avatars with applied styles; product photos with seasonal decorations; a "match our brand colors" button; the possibilities are endless! ## Conclusion As .NET developers, we can now generate AND edit images in just a few lines of code, thanks to `IImageGenerator`. And because it's part of Microsoft.Extensions.AI, everything we've already learned still applies: we can register it with Dependency Injection using `AddImageGenerator()`, and add middleware like logging and telemetry — exactly like we do with `IChatClient`. You can find the completed code sample here: [https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/6.%20Generating%20Images?ref=codetraveler.io) Next week, we'll answer the big question: how do we know if our AI's answers are any *good*? Enter `Microsoft.Extensions.AI.Evaluation`. Make sure to subscribe to this blog so you don't miss it! 👇 To learn even more about Microsoft Extensions AI, check out my course on Dometrain: [Learn to use Microsoft.Extensions.AI in .NET - DometrainMaster the main AI abstraction library for .NET AI development![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-32x32-ff036adf-4de9-4669-9acf-c8a3731e4f4a.png)DometrainBrandon Minnick![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/thumbnail-e14b39ca760b44139b0be9246741f6cb-4f6fb610-8874-49e1-af67-a9149c7f8cca.jpeg)](https://dometrain.com/course/from-zero-to-hero-microsoft-extensions-ai-in-dotnet?ref=codetraveler.io) ### Adding Semantic Search to .NET Apps with `IEmbeddingGenerator` URL: https://codetraveler.io/2026/07/20/adding-semantic-search-to-net-apps-with-iembeddinggenerator/ Last updated: 2026-07-20T10:22:08.000Z Have you ever typed "How do I get my money back?" into a search box and gotten zero results, because the docs only ever say "refund policy"? Embeddings fix this! And [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) makes it easy than ever to do. This is Part 2 of our `Microsoft.Extensions.AI` series. In [Part 1](https://codetraveler.io/2026/07/16/adding-ai-to-your-net-app-with-ichatclient/), we used `IChatClient` to build an LLM chat bot. This week, we'll use `IEmbeddingGenerator` to build search that understands *meaning*, not just keywords! You're welcome to skip the definitions and jump directly to the code below. ## What is an Embedding? An embedding is just the fancy Linear Alegrbra term for a numerical array (also known as a "vector") that captures the meaning of text. ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2026/07/image.png) Embedding Generator Input + Output In the above flow diagram, an Embedding Generator creates vectors (numerical arrays) for the the words `Apple`, `Robot` and `Ocean`. Context also matters when generating embeddings. For example, the word "Apple Computer" generates different embeddings than "Apple Pie". Comparing these vectors is the entire trick behind semantic search — and it's the foundation of a RAG Pipeline (Retrieval-Augmented Generation), a topic that we'll cover later in this series! ## What is IEmbeddingGenerator? `IEmbeddingGenerator>` is the MEAI abstraction for creating embeddings: text goes in, a vector comes out. Just like `IChatClient`, it's one interface that every AI provider (OpenAI, Azure, Ollama + more) plugs into, meaning we can swap providers without rewriting our code. ## Code Now that we have those pesky definitions out of the way, let's write some code! You can find the [completed code sample here](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/5.%20Ingesting%20Data?ref=codetraveler.io). ### Prerequisites If you'd like to follow along, here are the steps. I recommend [Ollama](https://ollama.com/?ref=codetraveler.io) because it's completely free. It runs open-source models locally on our machine, so there's no API key and no credit card required. 1. [Download + install Ollama](https://ollama.com/download?ref=codetraveler.io) (if you haven't already) 2. Download an embedding model by entering this command in the terminal: ```bash ollama pull qwen3-embedding ``` Now let's write some code! ### Console App First, let's create a `File`\->`New` Console app. 1. In Visual Studio, create a [File -> New Console App](https://learn.microsoft.com/dotnet/core/tutorials/with-visual-studio?ref=codetraveler.io) 2. Add the following NuGet Packages to the Console app: - [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) - [OllamaSharp](https://www.nuget.org/packages/OllamaSharp?ref=codetraveler.io) - [System.Numerics.Tensors](https://www.nuget.org/packages/System.Numerics.Tensors?ref=codetraveler.io) > If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). Let's embed four documents, then ask a question that shares ZERO keywords with the right answer: ```csharp using System.Numerics.Tensors; using Microsoft.Extensions.AI; using OllamaSharp; // OllamaApiClient implements IEmbeddingGenerator, the abstraction provided by Microsoft.Extensions.AI IEmbeddingGenerator> generator = new OllamaApiClient(new Uri("http://localhost:11434"), "qwen3-embedding"); var documents = [ "Our refund policy allows returns within 30 days of purchase.", "The espresso machine heats up in under two minutes.", "Shipping is free on all orders over fifty dollars.", "Reset your password from the account settings page." ]; // Embed every document, keeping each vector paired with its text var documentEmbeddings = await generator.GenerateAndZipAsync(documents); // Embed the question using the SAME model ReadOnlyMemory query = await generator.GenerateVectorAsync("How do I get my money back?"); // Rank the documents by Cosine Similarity (1.0 = identical meaning) var rankedEmbeddings = documentEmbeddings .Select(static doc => (doc.Value, Similarity: TensorPrimitives.CosineSimilarity(query.Span, doc.Embedding.Vector.Span))) .OrderByDescending(static result => result.Similarity); foreach (var (text, similarity) in rankedEmbeddings) { Console.WriteLine($"{similarity:F3} {text}"); } ``` Now let's click **Run** and see what gets printed to the console: ```bash 0.582 Our refund policy allows returns within 30 days of purchase. 0.161 Shipping is free on all orders over fifty dollars. 0.107 Reset your password from the account settings page. 0.084 The espresso machine heats up in under two minutes. ``` It works! The refund policy has the highest similarity score by a mile, even though our question never mentions the words "refund," "policy" or "returns." The model matched the MEANING! 🎉 > **Important:** Always embed your documents and your query with the *same* model. Vectors from different embedding models are not comparable to each other. ## Conclusion As .NET developers, we can now add semantic search to our apps with just a handful of lines of code, thanks to `IEmbeddingGenerator`. And we've only scratched the surface! MEAI also ships caching middleware for embeddings (so we never pay to embed the same text twice), and pairing embeddings with a Vector Store unlocks full RAG pipelines — both coming later in this series. You can find the completed code sample here: [https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai?ref=codetraveler.io) Next week, we'll use Microsoft.Extensions.AI to generate images! Make sure to subscribe to this blog so you don't miss it! 👇 To learn even more about Microsoft Extensions AI, check out my course on Dometrain: [Learn to use Microsoft.Extensions.AI in .NET - DometrainMaster the main AI abstraction library for .NET AI development![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-32x32-79dd68cc-7412-4e4a-9ff5-6c276b357a6c.png)DometrainBrandon Minnick![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/thumbnail-e14b39ca760b44139b0be9246741f6cb-b5126997-c8a2-495f-8633-780fe389e2f4.jpeg)](https://dometrain.com/course/from-zero-to-hero-microsoft-extensions-ai-in-dotnet/?utm%5Fsource=blog&utm%5Fmedium=cta&utm%5Fcampaign=meai-blog-series&utm%5Fcontent=iembeddinggenerator) [GitHub - Dometrain/from-zero-to-hero-microsoft-extensions-ai: The accompanying source code for the DomeTrain Course “From Zero to Hero: Microsoft.Extensions.Ai”The accompanying source code for the DomeTrain Course “From Zero to Hero: Microsoft.Extensions.Ai” - Dometrain/from-zero-to-hero-microsoft-extensions-ai![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-62de50ce-f6fd-418f-8b72-cd266357da62.svg)GitHubDometrain![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/from-zero-to-hero-microsoft-extensions-ai-a44273c9-2196-4c22-96d0-70c81ab32276)](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai?ref=codetraveler.io) ### Creating an AI Chat Bot in .NET with `IChatClient` URL: https://codetraveler.io/2026/07/16/adding-ai-to-your-net-app-with-ichatclient/ Last updated: 2026-07-22T16:19:34.000Z Are you looking to implement AI in your .NET app? [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) makes it easy! This is the first post in a new series where we'll learn `Microsoft.Extensions.AI` together, one building block at a time. Let's start with the most important building block of them all, `IChatClient`! You're welcome to skip the definitions and jump directly to the code below. ## What is Microsoft.Extensions.AI? [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai?ref=codetraveler.io) (aka "MEAI") is the recommended approach for all us .NET developers to integrate LLMs into our apps going forward. Under the hood, MEAI provides an abstraction layer that allows us to easily swap AI providers (OpenAI, Azure, Amazon Bedrock, Ollama + more) while keeping the code in our business logic and unit tests exactly the same. Think of it like `ILogger`, but for AI: we write our code once, then plug in any provider. ## What is IChatClient? `IChatClient` allows us to easiliy create an AI Chat Bot. It's an interface with two primary methods: - `GetResponseAsync` sends our messages to the LLM and returns its complete response - `GetStreamingResponseAsync` streams the response back token-by-token (the ChatGPT typewriter effect) That's it! Every AI provider plugs into these same two methods. ## Code Now that we have those pesky definitions out of the way, let's write some code! You can find the [completed code sample here](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/3.%20Introducing%20IChatClient?ref=codetraveler.io). ### Prerequisites If you'd like to follow along, here are the steps. I recommend [Ollama](https://ollama.com/?ref=codetraveler.io) because it's completely free. It runs open-source models locally on our machine, so there's no API key and no credit card required! 1. [Download + install Ollama](https://ollama.com/download?ref=codetraveler.io) 2. Download a model by entering this command in the terminal: ```bash ollama pull qwen3.5 ``` Now we can finally write some code! ### Console App First, let's create a File->New Console App: 1. Create a [File -> New Console App in Visual Studio](https://learn.microsoft.com/dotnet/core/tutorials/with-visual-studio?ref=codetraveler.io) 2. Add the [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI?ref=codetraveler.io) NuGet Package to the app 3. Add the [OllamaSharp](https://www.nuget.org/packages/OllamaSharp?ref=codetraveler.io) NuGet Package to the app > If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). ```csharp using Microsoft.Extensions.AI; using OllamaSharp; // OllamaApiClient implements IChatClient, the abstraction provided by Microsoft.Extensions.AI IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "qwen3.5"); ChatResponse response = await client.GetResponseAsync("Explain async/await in one sentence."); Console.WriteLine(response.Text); ``` Now let's click **Run** and see what gets printed to the console: ``` Async/await lets your code kick off a long-running task and keep working while it finishes, without blocking the thread. ``` It works! We just built an AI Chat Bot in 5 lines of code! ### Streaming Responses Waiting for the complete response can feel slow. Let's make our app feel alive by streaming the response token-by-token using `GetStreamingResponseAsync`: ```csharp await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync("Tell me a short story about a developer attending Microsoft Build for the first time.")) { Console.Write(update.Text); // Prints each token as it arrives, just like ChatGPT } ``` ### Swapping AI Providers Here's the best part: Because we wrote our code against the `IChatClient` interface, we can swap Ollama for OpenAI by changing ONE line of code. First, add the [Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI?ref=codetraveler.io) NuGet Package: ```bash dotnet add package Microsoft.Extensions.AI.OpenAI ``` Then update the one line where we create our `IChatClient`: ```csharp using Microsoft.Extensions.AI; using OpenAI; // Same IChatClient interface, different AI provider! IChatClient client = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")) .GetChatClient("gpt-5.1") .AsIChatClient(); ``` Every other line of our app stays exactly the same. No rewrites when we change AI providers — our future selves will thank us! 💯 ## Conclusion As .NET developers, we should all be leveraging `Microsoft.Extensions.AI` when adding GenAI to our apps. New models debut every month, and thanks to `IChatClient`, we'll never be locked in to yesterday's provider. And we've only scratched the surface! `IChatClient` can also remember our Chat History, invoke Function Calling (yes — the LLM can call our C# methods!), and add middleware like caching, logging and telemetry. You can find the completed code sample here: [https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai/tree/main/3.%20Introducing%20IChatClient?ref=codetraveler.io) Next week, we'll teach our apps to search by *meaning* using `IEmbeddingGenerator`. Make sure to subscribe to this blog so you don't miss it! 👇 To learn even more about Microsoft Extensions AI, check out my comprehensive course on Dometrain: [Learn to use Microsoft.Extensions.AI in .NET - DometrainMaster the main AI abstraction library for .NET AI development![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-32x32-ccf75acf-2ef4-4dd4-94ba-52d1b6885a4e.png)DometrainBrandon Minnick![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/thumbnail-e14b39ca760b44139b0be9246741f6cb-5f1b92d2-deb7-4ea3-bc21-125519a21482.jpeg)](https://dometrain.com/course/from-zero-to-hero-microsoft-extensions-ai-in-dotnet/?utm%5Fsource=blog&utm%5Fmedium=cta&utm%5Fcampaign=meai-blog-series&utm%5Fcontent=ichatclient) [GitHub - Dometrain/from-zero-to-hero-microsoft-extensions-ai: The accompanying source code for the DomeTrain Course “From Zero to Hero: Microsoft.Extensions.Ai”The accompanying source code for the DomeTrain Course “From Zero to Hero: Microsoft.Extensions.Ai” - Dometrain/from-zero-to-hero-microsoft-extensions-ai![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/favicon-c2c73f86-f648-4170-825e-18c7504e29b4.svg)GitHubDometrain![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/from-zero-to-hero-microsoft-extensions-ai-86237b6f-df88-417c-b3f8-84981306375f)](https://github.com/Dometrain/from-zero-to-hero-microsoft-extensions-ai?ref=codetraveler.io) ### Introducing Bindable Property Source Generators URL: https://codetraveler.io/2026/01/29/introducing-bindable-property-source-generators/ Last updated: 2026-02-05T19:27:22.000Z I am excited to announce two new source generators introduced in `CommunityToolkit.Maui v14.0.0` : - `[BindableProperty]` - `[AttachedBindableProperty]` These new source generators make it easier than ever to create a `BindableProperty` in your .NET MAUI apps. In fact, all Bindable Properties in `CommunityToolkit.Maui` are now automatically generated using these new source generators. Using these attributes, the .NET MAUI Community Toolkit will generate all of the boiler-plate code required for Bindable Properties and Attached Properties. ## Opt-into this Experimental Feature We have decided to release this feature using the `[Experimental]` attribute. This allows us to let the community test its features and provide feedback while giving us the flexibility to modify the API as requested. 1. In the `csproj` file of your .NET MAUI app, suppress the `MCTEXP001` : ```xml MCTEXP001 ``` In the mean-time, I will be writing more comprehensive documentation and writing Analyzers, similar to `CommunityToolkit.MVVM`, to help provide you the best coding experience! ## Using `[BindableProperty]` To leverage the Bindable Property Source Generator, first ensure you using a `partial class.` Then, add the `partial` keyword and attach the `[CommunityToolkit.Maui.BindableProperty]` attribute to your Property: 1. Add the `partial` keyword to the `class` 2. Add the `partial` keyword the Property for which to generate an associated Bindable Property 3. Add the `[CommunityToolkit.Maui.BindableProperty]` attribute to the Property for which to generate an associated Bindable Property Here is an example: ```csharp using CommunityToolkit.Maui; namespace BindablePropertySourceGeneratorSample; public partial class CustomButton : Button { [CommunityToolkit.Maui.BindableProperty] public partial int? Number { get; set; } } ``` The above example generates the following Bindable Property: ```csharp public partial class CustomButton { /// /// BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Number", typeof(int?), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public partial int? Number { get => (int)GetValue(NumberProperty); set => SetValue(NumberProperty, value); } } ``` ### Setting Default Value To set the default value for `[BindableProperty]`, simply set the initializer on the partial property: ```csharp [CommunityToolkit.Maui.BindableProperty] public partial int Number { get; set; } = 10; ``` ### Setting Default `BindingMode` To set the default `BindingMode` for `[BindableProperty]`, assign the value to the attribute's `DefaultBindingMode` property: ```csharp [CommunityToolkit.Maui.BindableProperty(DefaultBindingMode = BindingMode.TwoWay)] public partial int Number { get; set; } ``` ### Using `ValidateValue` To leverage the `ValidateValueDelegate`, use the `nameof()` operator to assign the name of the validation method to the the attribute's `ValidateValueMethodName` property: ```csharp [CommunityToolkit.Maui.BindableProperty(ValidateValueMethodName = nameof(ValidateNumber))] public partial int Number { get; set; } static bool ValidateNumber(BindableObject bindable, object value) { var updatedValue = (int)value; return updatedValue > 0; } ``` > **Important:** The method used for `ValidateValueMethodName` must adhere to the [ValidateValueDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.validatevaluedelegate?view=net-maui-9.0&ref=codetraveler.io) method signature: `static bool MethodName(BindableObject, object)` ### Using `PropertyChanging` To leverage the `BindingPropertyChangingDelegate`, use the `nameof()` operator to assign the name of the property changing method to the the attribute's `PropertyChangingMethodName` property: ```csharp [CommunityToolkit.Maui.BindableProperty(PropertyChangingMethodName = nameof(OnNumberChanging))] public partial int Number { get; set; } static void OnNumberChanging(BindableObject bindable, object oldValue, object newValue) { Trace.WriteLine($"Old Value: {oldValue}"); Trace.WriteLine($"New Value: {newValue}"); } ``` > **Important:** The method used for `PropertyChangingMethodName` must adhere to the [BindingPropertyChangingDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.bindingpropertychangingdelegate?view=net-maui-9.0&ref=codetraveler.io) method signature: `static void MethodName(BindableObject, object, object)` ### Using `PropertyChanged` To leverage the `BindingPropertyChangedDelegate`, use the `nameof()` operator to assign the name of the property changed method to the the attribute's `PropertyChangedMethodName` property: ```csharp [CommunityToolkit.Maui.BindableProperty(PropertyChangedMethodName = nameof(OnNumberChanged))] public partial int Number { get; set; } static void OnNumberChanged(BindableObject bindable, object oldValue, object newValue) { Trace.WriteLine($"Old Value: {oldValue}"); Trace.WriteLine($"New Value: {newValue}"); } ``` > **Important:** The method used for `PropertyChangedMethodName` must adhere to the [BindingPropertyChangedDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.bindingpropertychangeddelegate?view=net-maui-9.0&ref=codetraveler.io) method signature: `static void MethodName(BindableObject, object, object)` ### Using `CoreceValue` To leverage the `CoerceValueDelegate`, use the `nameof()` operator to assign the name of the coerce value changed method to the the attribute's `CoerceValueMethodName` property: ```csharp [CommunityToolkit.Maui.BindableProperty(CoerceValueMethodName = nameof(CoerceNumber))] public partial int Number { get; set; } static object CoerceNumber(BindableObject bindable, object value) { var updatedValue = (int)value; return Math.Clamp(updatedValue, 0, int.MaxValue); } ``` > **Important:** The method used for `CoerceValueMethodName` must adhere to the [CoreceValueDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.coercevaluedelegate?view=net-maui-9.0&ref=codetraveler.io) method signature: `static object MethodName(BindableObject, object)` ### Creating a Readonly Bindable Property The `[BindableProperty]` attribute will automatically generate a Readonly Bindable Property (eg [BindableProperty.CreateReadOnly()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.createreadonly?view=net-maui-10.0&ref=codetraveler.io)) when the accessibility modifier of the Property's setter is `protected`, `private protected` or `private`. For example, a Readonly Bindable Property will be generated for the following properties whose setter cannot be accessed outside of its class: ```csharp public partial class CustomButton : Button { [CommunityToolkit.Maui.BindableProperty] public partial int Number { get; protected set; } [CommunityToolkit.Maui.BindableProperty] public partial char Letter { get; private protected set; } [CommunityToolkit.Maui.BindableProperty] public partial string Text { get; private set; } } ``` ## Using `[AttachedBindableProperty]` Using `[AttachedBindableProperty]` is a bit different. This attribute is applied to either the Class or the Constructor, not a Property, as [Attached Properties don't have an associated Property](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/attached-properties?ref=codetraveler.io): > .NET Multi-platform App UI (.NET MAUI) attached properties enable an object to assign a value for a property that its own class doesn't define To leverage the Attached Bindable Property Source Generator, first ensure you using a `partial class.` Then, add `[CommunityToolkit.Maui.AttachedBindableProperty]` attribute to either the class or constructor: 1. Add the `partial` keyword to the `class` 2. Add the `[CommunityToolkit.Maui.AttachedBindableProperty]` attribute to the class or constructor for which to generate an associated Attached Bindable Property Here is an example: ```csharp namespace BindablePropertySourceGeneratorSample; [AttachedBindableProperty("Number")] public partial class CustomButton : Button { public CustomButton() { } } ``` Alternatively, the attribute can be attached to the constructor: ```csharp namespace BindablePropertySourceGeneratorSample; public partial class CustomButton : Button { [AttachedBindableProperty("Number")] public CustomButton() { } } ``` Both examples above generate the following Attached Bindable Property along with its associated Get/Set methods ```csharp public partial class CustomButton { /// /// Attached BindableProperty for the Number property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Number", typeof(int), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, (global::Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); /// /// Gets Number for the child element. /// public static int GetNumber(global::Microsoft.Maui.Controls.BindableObject bindable) => (int)bindable.GetValue(NumberProperty); /// /// Sets Number for the child element. /// public static void SetNumber(global::Microsoft.Maui.Controls.BindableObject bindable, int value) => bindable.SetValue(NumberProperty, value); } ``` ### Nullability In C#, `Attribute` does not yet support nullability for `T`. For example, `[AttachedBindableProperty]` will generate a compiler error. To generate an Attached Property for a nullable type, set `IsNullable` to `true`: ```csharp [AttachedBindableProperty("Number", IsNullable = true)] public partial class CustomButton : Button { } ``` Setting `IsNullable = true` will generate an Attached Property using a nullable `T?`: ```csharp public partial class CustomButton { /// /// Attached BindableProperty for the Number property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Number", typeof(int), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, (global::Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); /// /// Gets Number for the child element. /// public static int? GetNumber(global::Microsoft.Maui.Controls.BindableObject bindable) => (int? )bindable.GetValue(NumberProperty); /// /// Sets Number for the child element. /// public static void SetNumber(global::Microsoft.Maui.Controls.BindableObject bindable, int? value) => bindable.SetValue(NumberProperty, value); } ``` ### Setting Default Value To set the default value for `[AttachedBindableProperty]`, assign a compile-time constant to the attribute's `DefaultValue` property: ```csharp [AttachedBindableProperty("Number", DefaultValue = 10)] public partial class CustomButton : Button { } ``` ### Setting Default `BindingMode` To set the default `BindingMode` for `[BindableProperty]`, assign the value to the attribute's `DefaultBindingMode` property: ```csharp [AttachedBindableProperty("Number", DefaultBindingMode = BindingMode.TwoWay)] public partial class CustomButton : Button { } ``` ### Using `ValidateValue` To leverage the `ValidateValueDelegate`, use the `nameof()` operator to assign the name of the validation method to the the attribute's `ValidateValueMethodName` property: ```csharp [AttachedBindableProperty("Number", ValidateValueMethodName = nameof(ValidateNumber))] public partial class CustomButton : Button { static bool ValidateNumber(BindableObject bindable, object value) { var updatedValue = (int)value; return updatedValue > 0; } } ``` > **Important:** The method used for `ValidateValueMethodName` must adhere to the [ValidateValueDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.validatevaluedelegate?view=net-maui-9.0&ref=codetraveler.io) method signature: `static bool MethodName(BindableObject, object)` ### Using `PropertyChanging` To leverage the `BindingPropertyChangingDelegate`, use the `nameof()` operator to assign the name of the property changing method to the the attribute's `PropertyChangingMethodName` property: ```csharp [AttachedBindableProperty("Number", PropertyChangingMethodName = nameof(OnNumberChanging))] public partial class CustomButton : Button { static void OnNumberChanging(BindableObject bindable, object oldValue, object newValue) { Trace.WriteLine($"Old Value: {oldValue}"); Trace.WriteLine($"New Value: {newValue}"); } } ``` > **Important:** The method used for `PropertyChangingMethodName` must adhere to the [BindingPropertyChangingDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.bindingpropertychangingdelegate?view=net-maui-9.0&ref=codetraveler.io) method signature: `static void MethodName(BindableObject, object, object)` ### Using `PropertyChanged` To leverage the `BindingPropertyChangedDelegate`, use the `nameof()` operator to assign the name of the property changed method to the the attribute's `PropertyChangedMethodName` property: ```csharp [AttachedBindableProperty("Number", PropertyChangedMethodName = nameof(OnNumberChanged))] public partial class CustomButton : Button { static void OnNumberChanged(BindableObject bindable, object oldValue, object newValue) { Trace.WriteLine($"Old Value: {oldValue}"); Trace.WriteLine($"New Value: {newValue}"); } } ``` > **Important:** The method used for `PropertyChangedMethodName` must adhere to the `BindingPropertyChangedDelegate` method signature: `static void MethodName(BindableObject, object, object)` ### Using `CoreceValue` To leverage the `CoerceValueDelegate`, use the `nameof()` operator to assign the name of the coerce value method to the the attribute's `CoerceValueMethodName` property: ```csharp [AttachedBindableProperty("Number", CoerceValueMethodName = nameof(CoerceNumber))] public partial class CustomButton : Button { static object CoerceNumber(BindableObject bindable, object value) { var updatedValue = (int)value; return Math.Clamp(updatedValue, 0, int.MaxValue); } } ``` > **Important:** The method used for `CoerceValueMethodName` must adhere to the [CoreceValueDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.coercevaluedelegate?view=net-maui-9.0&ref=codetraveler.io) method signature: `static object MethodName(BindableObject, object)` ### Using `CreateDefaultValue` To leverage the [CreateDefaultValueDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.createdefaultvaluedelegate-2?view=net-maui-9.0&ref=codetraveler.io), use the `nameof()` operator to assign the name of the create default value method to the the attribute's `DefaultValueCreatorMethodName` property: ```csharp [AttachedBindableProperty("Number", DefaultValueCreatorMethodName = nameof(CreateDefaultNumber))] public partial class CustomButton : Button { static object CreateDefaultNumber() => 10; } ``` > **Important:** The method used for `DefaultValueCreatorMethodName` must adhere to the [CreateDefaultValueDelegate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.createdefaultvaluedelegate-2?view=net-maui-9.0&ref=codetraveler.io) method signature: `static object MethodName()` ### Modifying Accessibility By default, `[AttachedBindableProperty]` will generate a `public` Bindable Property, a `public` Getter method and a `public` Setter method . To modify the accessibility of the Attached Property, set the value of the attribute's `BindablePropertyAccessibility`, `GetterAccessibility` and `SetterAccessibility` properties: ```csharp [AttachedBindableProperty("Number", BindablePropertyAccessibility = AccessModifier.Internal, GetterAccessibility = AccessModifier.Internal, SetterAccessibility = AccessModifier.Internal)] public partial class CustomButton : Button { } ``` This example will generate the following code: ```csharp public partial class CustomButton { /// /// Attached BindableProperty for the Number property. /// internal static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Number", typeof(int), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, (global::Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); /// /// Gets Number for the child element. /// internal static int GetNumber(global::Microsoft.Maui.Controls.BindableObject bindable) => (int)bindable.GetValue(NumberProperty); /// /// Sets Number for the child element. /// internal static void SetNumber(global::Microsoft.Maui.Controls.BindableObject bindable, int value) => bindable.SetValue(NumberProperty, value); } ``` ### Creating a Readonly Attached Property The `[AttachedBindableProperty]` attribute will automatically generate a Readonly Attached Bindable Property (eg [BindableProperty.CreateAttachedReadOnly()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty.createattachedreadonly?view=net-maui-10.0&ref=codetraveler.io)) when the `SetterAccessibility` value is set to `AccessModifier.Protected`, `AccessModifier.PrivateProtected` or `AccessModifier.Private`. For example, a Readonly Attached Property will be generated for the following: ```csharp [AttachedBindableProperty("Number", SetterAccessibility = AccessModifier.Protected)] [AttachedBindableProperty("Letter", SetterAccessibility = AccessModifier.PrivateProtected)] [AttachedBindableProperty("Text", SetterAccessibility = AccessModifier.Private)] public partial class CustomButton : Button { } ``` ## Modifying XML Documentation By default, `[AttachedBindableProperty]` will generate the boiler-plate XML Documentation seen in examples above. To customize the XML Documentation generated, pass in the XML Documentation as a `string` to the attribute's `BindablePropertyXmlDocumentation` property, `GetterMethodXmlDocumentation` property and `SetterMethodXmlDocumentation` property accordingly: ```csharp [AttachedBindableProperty("Number", BindablePropertyXmlDocumentation = NumberBindablePropertyXmlDocumentation, GetterMethodXmlDocumentation = NumberGetterXmlDocumentation, SetterMethodXmlDocumentation = NumberSetterXmlDocumentation)] public partial class CustomButton : Button { const string NumberBindablePropertyXmlDocumentation = """ ///The Bindable Property Number for a """; const string NumberGetterXmlDocumentation = """ ///Sets the value for a """; const string NumberSetterXmlDocumentation = """ ///Gets the value for a ///Number cannot be less than zero """; } ``` > **Important:** *The `string` used for the XML Documentation must contain both `///`* # Conclusion I hope you enjoy using these new source generators! They were a ton of work to create, but I'm excited to see these bring a giant productivity boost to all developers in the .NET MAUI Community. Please give us your feedback by opening a [Discussion on the .NET MAUI Community Toolkit GitHub repository](https://github.com/CommunityToolkit/Maui/discussions?ref=codetraveler.io). To see more examples of \`\[BindableProperty\] and `[AttachedBindableProperty]`, check out the source code for [CommunityToolkit.Maui](https://github.com/CommunityToolkit/Maui/tree/main/src/CommunityToolkit.Maui?ref=codetraveler.io) to see how we're now using it to generate all of the Bindable Properties in our library. ### Introducing AWSSDK.Extensions.Bedrock.MEAI URL: https://codetraveler.io/2025/03/25/introducing-awssdk-extensions-bedrock-meai-2/ Last updated: 2025-03-25T21:55:51.000Z Are you looking to add GenAI functionality to your .NET app? [AWSSDK.Extensions.Bedrock.MEAI](https://www.nuget.org/packages/AWSSDK.Extensions.Bedrock.MEAI/?ref=codetraveler.io) makes it easy. Let's define what some of these words mean, then check out how to use this NuGet Package in our apps! You're also welcome to skip the definitions and jump directly to the code below. ### What is MEAI? "MEAI" in the package name is an acronym for [Microsoft Extensions AI](https://www.nuget.org/packages/Microsoft.Extensions.AI/?ref=codetraveler.io) a new library that is now the recommended approach for all us .NET developers to integrate GenAI into our apps going forward. Under the hood, MEAI provides an abstraction layer that allows us to easily swap GenAI tools and models while keeping the code in our business logic and unit tests exactly the same. If you're not already using [Microsoft AI Extensions for .NET](https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/?ref=codetraveler.io), I HIGHLY recommend implementing it; your future self will thank you. 💯 ### What is Bedrock? "Bedrock" in the package name refers to [Amazon Bedrock](https://aws.amazon.com/bedrock?trk=26a307dd-f6c6-4133-a99f-0388d1304aef&sc%5Fchannel=el&ref=codetraveler.io), an [AWS](https://aws.amazon.com/?trk=26a307dd-f6c6-4133-a99f-0388d1304aef&sc%5Fchannel=el&ref=codetraveler.io) service that provides us [access to over 40+ foundation models](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html?trk=26a307dd-f6c6-4133-a99f-0388d1304aef&sc%5Fchannel=el&ref=codetraveler.io) from popular providers such as Anthropic, Meta, Mistral, DeepSeek and more. Just for fun, let's ask the recently released [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html?trk=26a307dd-f6c6-4133-a99f-0388d1304aef&sc%5Fchannel=el&ref=codetraveler.io), to get a better explanation: ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2025/03/ScreenFlow-2.gif) ## Code Now that we have those pesky definitions out of the way, let's create a .NET Console app that uses `AWSSDK.Extensions.Bedrock.MEAI`! You can find the completed code sample here: [https://github.com/TheCodeTraveler/Bedrock-MEAI-Sample](https://github.com/TheCodeTraveler/Bedrock-MEAI-Sample?ref=codetraveler.io) Let's first create a [File -> New Console App in Visual Studio](https://learn.microsoft.com/en-us/dotnet/core/tutorials/with-visual-studio?ref=codetraveler.io). Next, add the [AWSSDK.Extensions.Bedrock.MEAI NuGet Package](https://www.nuget.org/packages/AWSSDK.Extensions.Bedrock.MEAI?ref=codetraveler.io) to our app. If you're unfamiliar with adding NuGet Packages, you can find more information [here](https://learn.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio?ref=codetraveler.io#find-and-install-a-package). Now we can finally write some code! ```cs using AiChatClient; using Amazon; using Amazon.BedrockRuntime; using Microsoft.Extensions.AI; IAmazonBedrockRuntime runtime = new AmazonBedrockRuntimeClient("Insert AwsCredentials AccessKeyId", "Insert AwsCredentials SecretAccessKey", RegionEndpoint.USEast1); IChatClient client = runtime.AsChatClient(); // Abstraction provided by Microsoft.Extensions.AI var chatMessage = new ChatMessage { Text = "Is this working?" }; await foreach (var response in client.GetStreamingResponseAsync(chatMessage, new() { ModelId = "anthropic.claude-v2.1" })) { Console.Write(response.Text); } ``` Now let's click **Run** and see what happens 👀 ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2025/03/ScreenFlow-1.gif) It works! And the crazy part is it only took a couple lines of code! You can find the completed code sample here: [https://github.com/TheCodeTraveler/Bedrock-MEAI-Sample](https://github.com/TheCodeTraveler/Bedrock-MEAI-Sample?ref=codetraveler.io) ## Conclusion As .NET developers, we should all be leveraging the [Microsoft AI Extensions for .NET](https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/?ref=codetraveler.io) when adding GenAI to our apps. As new LLMs debut and as new GenAI services, like [Amazon Bedrock](https://aws.amazon.com/bedrock?trk=26a307dd-f6c6-4133-a99f-0388d1304aef&sc%5Fchannel=el&ref=codetraveler.io) continue to grow, we are all but guaranteed that we'll be swapping models and services in our production apps for the foreseeable future. But thanks to tools like [AWSSDK.Extensions.Bedrock.MEAI](https://www.nuget.org/packages/AWSSDK.Extensions.Bedrock.MEAI?ref=codetraveler.io), Next, I'll be exploring how to use this to build a ChatGPT clone using `AWSSDK.Extensions.Bedrock.MEAI` \+ .NET MAUI. Make sure to subscribe to this blog and to watch this GitHub repo for the latest updates! 👇 [GitHub - TheCodeTraveler/Bedrock-MEAI-Sample: A sample application demonstrating AWSSDK.Extensions.Bedrock.MEAIA sample application demonstrating AWSSDK.Extensions.Bedrock.MEAI - TheCodeTraveler/Bedrock-MEAI-Sample![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/icon/pinned-octocat-093da3e6fa40-15.svg)GitHubTheCodeTraveler![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/thumbnail/Bedrock-MEAI-Sample)](https://github.com/TheCodeTraveler/Bedrock-MEAI-Sample?ref=codetraveler.io) ### Upgrading from Xamarin.CommunityToolkit.Markup to CommunityToolkit.Maui.Markup URL: https://codetraveler.io/2024/07/29/upgrading-from-xamarin-communitytoolkit-markup-to-communitytoolkit-maui-markup/ Last updated: 2024-07-29T22:55:23.000Z In `Xamarin.Forms`, we used [Xamarin.CommunityToolkit.Markup](https://www.nuget.org/packages/Xamarin.CommunityToolkit.Markup/?ref=codetraveler.io) to create our UI using C# (no XAML). For [.NET MAUI](https://devblogs.microsoft.com/dotnet/announcing-dotnet-maui-in-dotnet-8/?ref=codetraveler.io), we can continue to create our UIs in C# (No XAML) thanks to [CommunityToolkit.Maui.Markup](https://www.nuget.org/packages/CommunityToolkit.maui.markup?ref=codetraveler.io). Let's take a look at the key differences between the two libraries, and then we'll learn how to get started! ### Typed Bindings The biggest update in `CommunityToolkit.Maui` is the introduction of Typed Bindings. With Typed Bindings we can directly reference the property in our ViewModel, avoiding using reflection when `INotifyPropertyChanged` is called: ```csharp new Label() .Bind(Label.TextProperty, getter: (MyViewModel vm) => vm.LabelText, setter: (MyViewModel vm, string text) => vm.LabelText = text); ``` With Typed Bindings, we can also include a converter inline, eliminating the need to create a separate `IValueConverter`: ```csharp new Label() .Bind(Label.TextProperty, getter: (MyViewModel vm) => vm.LabelText, setter: (MyViewModel vm, string text) => vm.LabelText = text, convert: (string textFromViewModel) => textFromViewModel.Trim(), convertBack: (string textFromView) => textFromView.Trim()); ``` Learn more about the Typed Bindings here: [https://learn.microsoft.com/dotnet/communitytoolkit/maui/markup/extensions/bindable-object-extensions#bind](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/markup/extensions/bindable-object-extensions?ref=codetraveler.io#bind) #### Relative Bindings While Typed Bindings are great, they do not support [Relative Bindings](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/relative-bindings?ref=codetraveler.io). To leverage `RelativeBindingSource`, we continue to use the "old" binding engine: ```csharp new Label() .Bind(Label.TextProperty, nameof(MyViewModel.LabelText), source: new RelativeBindingSource(RelativeBindingSourceMode.FindAncestorBindingContext, typeof(MyViewModel)) ``` # Getting Started ## Install CommunityToolkit.Maui.Markup In your .NET MAUI `csproj` file, add the following package reference to `CommunityToolkit.Maui.Markup`: ```xml ``` ## Install CommunityToolkit.MVVM I highly recommend also installing the MVVM Community Toolkit. The MVVM Community Toolkit allows us to remove tons of MVVM boiler-plate code, replacing many lines of code with one attribute, like `[ObservableProperty]` and `[RelayCommand]`. You can see how I've used the MVVM Toolkit attributes in the sample code of my ViewModel down below. In your .NET MAUI `csproj` file, add the following package reference to `CommunityToolkit.Maui.Markup`: ```xml ``` Learn more about the MVVM Community Toolkit here: [https://learn.microsoft.com/dotnet/architecture/maui/mvvm-community-toolkit-features](https://learn.microsoft.com/en-us/dotnet/architecture/maui/mvvm-community-toolkit-features?ref=codetraveler.io) ## Creating UI in C# For this example, let's re-write `MainPage.xaml` that is included in the template when we create a `File -> New` .NET MAUI app using Visual Studio. ```csharp class MainPage : ContentPage { public MainPage(MainViewModel vm) { BindingContext = vm; Content = new ScrollView { Content = new VerticalStackLayout { Spacing = 25, Padding = 30, Children = { new Label() .Text("Hello World") .Font(size: 32) .CenterHorizontal(), new Label() .Text("Welcome to .NET MAUI Markup Community Toolkit Sample") .Font(size: 18) .CenterHorizontal(), new Label() .Font(size: 18, bold: true) .CenterHorizontal() .Bind(Label.TextProperty, static (MainViewModel vm) => vm.ClickCount, convert: count => $"Current Count: {count}"), new Button() .Text("Click Me") .Font(bold: true) .CenterHorizontal() .Bind(Button.CommandProperty, static (MainViewModel vm) => vm.IncrementClickMeButtonCommand, mode: BindingMode.OneTime), new Image() .Source("dotnet_bot") .Size(250, 310) .CenterHorizontal() } } }; } } partial class MainViewModel : ObservableObject { [ObservableProperty] int _clickCount = 0; [RelayCommand] void IncrementClickMeButton() => ClickCount++; } ``` You can find the completed open-source sample here: [https://github.com/brminnick/HelloMauiMarkup](https://github.com/brminnick/HelloMauiMarkup?ref=codetraveler.io) # Resources [GitHub - CommunityToolkit/Maui.Markup: The .NET MAUI Markup Community Toolkit is a community-created library that contains Fluent C# Extension Methods to easily create your User Interface in C#The .NET MAUI Markup Community Toolkit is a community-created library that contains Fluent C# Extension Methods to easily create your User Interface in C# - CommunityToolkit/Maui.Markup![](https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg)GitHubCommunityToolkit![](https://opengraph.githubassets.com/869f64fa518c1f246bf541eabdc1fef3c57a738feffd7a1cac8fa0581a1f835e/CommunityToolkit/Maui.Markup)](https://github.com/CommunityToolkit/Maui.Markup?ref=codetraveler.io) [GitHub - brminnick/HelloMauiMarkup: An iOS, Android, macOS + Windows app built using .NET MAUI, demonstrating how to use the .NET MAUI Markup Community ToolkitAn iOS, Android, macOS + Windows app built using .NET MAUI, demonstrating how to use the .NET MAUI Markup Community Toolkit - brminnick/HelloMauiMarkup![](https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg)GitHubbrminnick![](https://opengraph.githubassets.com/013db9d09ef2d22fa4c4379d05035985048ae6adde5f38e57e2b5793bdb7eae2/brminnick/HelloMauiMarkup)](https://github.com/brminnick/HelloMauiMarkup?ref=codetraveler.io) ### .NET MAUI Training Courses URL: https://codetraveler.io/2024/03/14/from-zero-to-hero-net-maui/ Last updated: 2024-03-14T17:43:36.000Z I'm excited to announce 2 new .NET MAUI courses: [Getting Started with .NET MAUI](https://dometrain.com/bundle/from-zero-to-hero-dotnet-maui/?ref=codetraveler.io) and [Deep Dive into .NET MAUI](https://dometrain.com/bundle/from-zero-to-hero-dotnet-maui/?ref=codetraveler.io)! These courses contain +10 hours of video content and code examples to help you build your very first .NET MAUI app and then deep dive into the best practices for being a successful .NET MAUI developer! I'm very proud of these courses and know that you'll love them too ❤️ If you have any friends or colleagues looking to learn how to make iOS + Android apps for C#, send them this way! [![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2024/03/cover-2.png)](https://dometrain.com/bundle/from-zero-to-hero-dotnet-maui/?ref=codetraveler.io) [![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2024/03/cover-3.png)](https://dometrain.com/bundle/from-zero-to-hero-dotnet-maui/?ref=codetraveler.io) ### Supporting Android v12+ Splash Screens in Xamarin.Forms URL: https://codetraveler.io/2022/08/16/supporting-android-splash-screens-in-xamarin-forms/ Last updated: 2022-08-16T03:10:31.000Z If you're an Android user, you may have noticed that, starting with Android v12.0, when you launch an app its Splash Screen now just looks like it's logo. Strange, right? Well that's apparently how Android wants it because that's what they're forcing us to do! Here's an example of how the splash screen looks in my [open-source Xamarin.Forms app, GitTrends](https://gittrends.com/?ref=codetraveler.io), before and after Android v12.0\. It's a subtle difference (once fixed), but you'll notice the new splash screen now exactly matches the GitTrends icon and is a smaller dpi due to Android's new splash screen restrictions: | Before Android v12.0 | After Android v12.0 (broken) | After Android v12.0 (Fixed) | | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ![ScreenFlow2](https://user-images.githubusercontent.com/13558917/184755522-bcadc661-79ef-4720-bca6-fa94153baa19.gif) | ![ScreenFlow](https://user-images.githubusercontent.com/13558917/184790130-8e02e607-9bba-4acc-882f-b3802dc1815c.gif) | ![ScreenFlow](https://user-images.githubusercontent.com/13558917/184742776-2a3bf115-1ab0-4f5d-8c43-06cf0aafe6f0.gif) | ## Step 0: Target Android API 31+ #### AndroidManifest.xml In `[AndroidManifest.xml](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Properties/AndroidManifest.xml?ref=codetraveler.io#L3)`, ensure the `android:targetSdkVersion` is `31` or higher (as of publishing this, the current stable release of Android is API Level 32). Here's an example from [GitTrends' AndroidMainifest.xml](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Properties/AndroidManifest.xml?ref=codetraveler.io#L3): ```xml ``` In the [Android csproj file](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/GitTrends.Android.csproj?ref=codetraveler.io#L18), eg `MyApp.Android.csproj`, ensure `TargetFrameworkVersion` is set to `v12.0` or higher (as of publishing this, the current stable release of Android is v12.1). Here's an example from [GitTrends.Android.csproj](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/GitTrends.Android.csproj?ref=codetraveler.io#L18): ```xml v12.1 ``` ## Step 1: Create New Images I know...this is a pain. Android already makes us create multiple images for each screen density (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi, etc), and we need to do it again for our new Splash Screen icon. ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2022/08/68747470733a2f2f646576656c6f7065722e616e64726f69642e636f6d2f7374617469632f61626f75742f76657273696f6e732f31322f696d616765732f73706c6173682d73637265656e2d69636f6e2d64696d656e73696f6e732e706e67.png) The splash screen icon should match our app's icon and it should reside inside of a 192dp circle centered inside of a 288dp `png` file. These are the steps I followed for [GitTrends](https://gittrends.com/?ref=codetraveler.io): 1. For each display density, create a new `.png` file of your app icon, sized 288dp - Use [dp to px calculator](http://labs.rampinteractive.co.uk/android%5Fdp%5Fpx%5Fcalculator/?ref=codetraveler.io) to determine the pixel size for each screen density 2. Inside each new file, for each display density, center the app icon inside of a 192dp-radius circle - Use [dp to px calculator](http://labs.rampinteractive.co.uk/android%5Fdp%5Fpx%5Fcalculator/?ref=codetraveler.io) to determine the pixel size for each screen density 3. Add the new files to the corresponding `drawable` folder, eg `/Resources/drawable-hdpi/`, `/Resources/drawable-xhdpi/`, etc For my app, [GitTrends](https://gittrends.com/?ref=codetraveler.io), I used the tool [Sketch](https://www.sketch.com/?ref=codetraveler.io) to create my new Splash Screen images. You're welcome to leverage the templates I used in my [Icon.sketch file](https://github.com/brminnick/GitTrends/blob/main/Artwork/Icon.sketch?ref=codetraveler.io): [https://github.com/brminnick/GitTrends/blob/main/Artwork/Icon.sketch](https://github.com/brminnick/GitTrends/blob/main/Artwork/Icon.sketch?ref=codetraveler.io) ## Step 2: Create a new `adaptive-icon` The new splash screen requires an `adaptive-icon` resource, similar to how we create `[icon.xml](https://github.com/brminnick/GitTrends/blob/main/GitTrends.Android/Resources/mipmap-anydpi-v26/icon.xml?ref=codetraveler.io)` and `[icon-round.xml](https://github.com/brminnick/GitTrends/blob/main/GitTrends.Android/Resources/mipmap-anydpi-v26/icon%5Fround.xml?ref=codetraveler.io)`. However, the [adaptive-icon API was added in Android API v26](https://developer.android.com/guide/practices/ui%5Fguidelines/icon%5Fdesign%5Fadaptive?ref=codetraveler.io). To maintain backwards compatibility with previous versions of Android (my [GitTrends](https://gittrends.com/?ref=codetraveler.io) app currently supports Android API Levels 23+), we'll create a `/drawable-v26/` folder for our new splash screen `xml` resource. This tells Android to only include the resource in v26 and up. The rule is that files included in `drawable-vXX` will only be included for devices running Android API Level XX and up. Here's the steps I followed 1. Create a new folder, `/Resources/drawable-v26/` 2. In `/Resources/drawable-v26/` add a new file, `splashscreenanimatedicon.xml` 3. In `splashscreenanimatedicon.xml`, create a new `adaptive-icon`: ```xml ``` If you're curious, you can find the [completed GitTrends' splashscreenanimatedicon.xml file](https://github.com/brminnick/GitTrends/blob/main/GitTrends.Android/Resources/drawable-v26/SplashScreenAnimatedIcon.xml?ref=codetraveler.io) here: [https://github.com/brminnick/GitTrends/blob/main/GitTrends.Android/Resources/drawable-v26/SplashScreenAnimatedIcon.xml](https://github.com/brminnick/GitTrends/blob/main/GitTrends.Android/Resources/drawable-v26/SplashScreenAnimatedIcon.xml?ref=codetraveler.io) ## Step 3: Update `styles.xml` #### Splash Screen Style, Before Android v12.0 In the file `/Resouces/values/styles.xml`, you likely have defined a custom splash screen for your app that looks something like this: ```xml ``` The `android:windowBackground` value points to my original splash screen, `splash_screen.xml` which I created using a `layer-list`. For example, here's what my original splash screen, `splash_screen.xml`, looked like before Android v12.0: ```xml ``` #### Splash Screen Style, After Android v12.0 For Android v12.0+, we need to add two values to our Splash Screen Theme in `styles.xml`: - `android:windowSplashScreenBackground` - The background color of your splash screen - `android:windowSplashScreenAnimatedIcon` - The `adaptive-icon` xml file we created above in Step 2 Here's the final final result from my [GitTrends](https://gittrends.com/?ref=codetraveler.io) app: ```xml ``` The best news is that we can keep the values for our previous splash screen, e.g. `android:windowBackground`, along side the new values for our Android API Level 30+ splash screen, `android:windowSplashScreenAnimatedIcon` and `android:windowSplashScreenBackground`! When the app runs on Android API Level 31+, it checks the Launch Theme for two values: - [android:windowSplashScreenBackground](https://github.com/brminnick/GitTrends/blob/438d8b7854bb1e9cab31e05f18d5bbdf6b198a3c/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L30) - [android:windowSplashScreenAnimatedIcon](https://github.com/brminnick/GitTrends/blob/438d8b7854bb1e9cab31e05f18d5bbdf6b198a3c/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L31) Android uses these two values for the Splash Screen and ignores the other values in the `style`. When the app runs on Android API Level 30 (and below), it doesn't know about the new APIs added in v31, so when it sees `android:windowSplashScreenAnimatedIcon` and `android:windowSplashScreenBackground`, it ignores these values. Android just checks the Launch Theme for [android:windowBackground](https://github.com/brminnick/GitTrends/blob/438d8b7854bb1e9cab31e05f18d5bbdf6b198a3c/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L28), which still points to my original launch screen, [splash\_screen.xml](https://github.com/brminnick/GitTrends/blob/main/GitTrends.Android/Resources/drawable/splash%5Fscreen.xml?ref=codetraveler.io), which uses a `layer-list`. ## Step 4: Set The Launch Theme Now that we've updated our splash screen theme, we need to ensure Android knows that we want to use it when the app launches. In Xamarin.Android, this may be set in multilple different places: - MainActivity.cs - AndroidManifest.xml - MainApplication.cs #### MainActivity.cs In `MainActivity.cs`, the launch theme can be set using the `[Activity]` attribute, eg `[Activity(MainTheme = "@style/MainTheme")]`. In your code, open `MainActivity.cs` and check to see if your MainActivity is using the `[Activity]` attribute (most Xamarin.Forms apps do, but some don't and that's ok). If the `[Activity]` attribute is being used, ensure that its `MainTheme` property references the name of your launch theme. For example, in [MainActivity.cs for GitTrends](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/MainActivity.cs?ref=codetraveler.io#L17), I used `[Activity(MainTheme = "@style/LaunchTheme")]` because I named the[ launch theme for my ](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L27)[GitTrends app](https://gittrends.com/?ref=codetraveler.io) `LaunchTheme`: ```xml ``` #### AndroidManifest.xml In `AndroidManifest.xml`, the launch theme can be set in the `` tag, eg ``. In your code, open `AndroidManifest.xml` and check to see if your Android Manifest is using the `android:theme` value inside of the `` tag (some Xamarin.Forms apps do, but some don't and that's ok). If the `android:theme` value is set, ensure that its `MainTheme` property references the name of your launch theme. For example, I named the[ launch theme for my ](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L27)[GitTrends app](https://gittrends.com/?ref=codetraveler.io) `LaunchTheme`, so I would thus use `` in my `AndroidManifest` (I personally don't because I already defined the launch theme in my `MainActivity`). #### MainApplication In `MainApplication.cs`, the launch theme can be set using the `[Application]` attribute, eg `[Application(MainTheme = "@style/MainTheme")]`. In your code, open `MainActivity.cs` and check to see if your MainActivity is using the `[Application]` attribute (most Xamarin.Forms apps do, but some don't and that's ok). If the `[Application]` attribute is being used, ensure that its `MainTheme` property references the name of your launch theme. For example, I named the[ launch theme for my ](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L27)[](https://gittrends.com/?ref=codetraveler.io)[GitTrends app](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L27)[ ](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L27)`[LaunchTheme](https://github.com/brminnick/GitTrends/blob/7ba4e4dcecb6c5d1c3089812e01deb22a6ca9c38/GitTrends.Android/Resources/values/styles.xml?ref=codetraveler.io#L27)`, so I would thus use `` in my `MainApplication` (I personally don't because I already defined the launch theme in my `MainActivity`). ## Conclusion Now we have a working Splash Screen on Andorid (again)! This new splash screen format actually opens the door for us to explore really cool animated splash screens. For example check out the below splash screens by [](https://www.raywenderlich.com/u/jemmsla?ref=codetraveler.io)[J](https://www.raywenderlich.com/32555180-splash-screen-tutorial-for-android?ref=codetraveler.io)emma Slater. If you're interested in learning more about how to continue to improve your new Android v12.0 splash screen, I highly recommend the following tutorials: - ["Splash Screen Tutorial for Android" by Jemma Slater ](https://www.raywenderlich.com/32555180-splash-screen-tutorial-for-android?ref=codetraveler.io) - ["Splash screens" by Google](https://developer.android.com/guide/topics/ui/splash-screen?ref=codetraveler.io) | Icon Background Color | Branding Image (bottom image) | Animated Splash Screen | | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | ![](https://koenig-media.raywenderlich.com/uploads/2022/03/splashscreen_9_splash_with_background.png) | ![](https://koenig-media.raywenderlich.com/uploads/2022/03/splashscreen_14_branding_image.png) | ![](https://koenig-media.raywenderlich.com/uploads/2022/03/splashscreen_18_exit_anim_demo.gif) | ### Using .NET MAUI Community Toolkit URL: https://codetraveler.io/2021/10/18/using-net-maui-community-toolkit-2/ Last updated: 2021-11-10T21:21:55.000Z ## Installing the .NET MAUI Community Toolkits There are currently two Community Toolkits focused on .NET MAUI: - [**CommunityToolkit.Maui**](https://www.nuget.org/packages/CommunityToolkit.Maui/?ref=codetraveler.io) - A collection of Animations, Behaviors, Converters and UI Controls for development with .NET MAUI. It simplifies and demonstrates common developer tools building iOS, Android, macOS and Windows apps with .NET MAUI - Sample App: [https://github.com/brminnick/HelloMauiToolkit](https://github.com/brminnick/HelloMauiToolkit?ref=codetraveler.io) - [**CommunityToolkit.Maui.Markup**](https://www.nuget.org/packages/CommunityToolkit.Maui.Markup/?ref=codetraveler.io) - A set of fluent helper methods to simplify building declarative .NET MAUI user interfaces in C# - Sample App: [https://github.com/brminnick/HelloMauiMarkup](https://github.com/brminnick/HelloMauiMarkup?ref=codetraveler.io) Both NuGet Packages can be added to any project targeting .NET 6 or later: 1. Open a `net6.0` project in Visual Studio 2. In the [Visual Studio Package Manager Console](https://docs.microsoft.com/nuget/consume-packages/install-use-packages-powershell?ref=codetraveler.io#opening-the-console-and-console-controls?WT.mc%5Fid=mobile-39516-bramin), enter the following command: ```bash Install-Package CommunityToolkit.Maui ``` or ```bash Install-Package CommunityToolkit.Maui.Markup ``` ## Converters The Converters found in `[ComunityToolkit.Maui](https://github.com/CommunityToolkit/Maui/blob/main/src/CommunityToolkit.Maui/Converters/ColorToStringConverter.shared.cs?ref=codetraveler.io)` can be easiliy added to our [MVVM Bindings](https://docs.microsoft.com/xamarin/xamarin-forms/xaml/xaml-basics/data-bindings-to-mvvm?WT.mc%5Fid=dotnet-45066-bramin&ref=codetraveler.io) allowing us to easily convert the value between our ViewModel and our View. Some of the popular converters include: - `InvertedBoolConverter` - `ListToStringConverter` - `TextCaseConverter` - ...and more! Check out this sample app to see how to use `CommunityToolkit.Maui.Converters`: [https://github.com/brminnick/HelloMauiToolkit](https://github.com/brminnick/HelloMauiToolkit?ref=codetraveler.io) #### Using Converters in .NET MAUI Community Toolkit, XAML 1. Install the .NET MAUI Community Toolkit NuGet Package ```bash Install-Package CommunityToolkit.Maui ``` 2. In XAML, add the following [XML Namespace](https://www.w3schools.com/xml/xml%5Fnamespaces.asp?ref=codetraveler.io): ```xml xmlns:converters="clr-namespace:CommunityToolkit.Maui.Converters;assembly=CommunityToolkit.Maui" ``` 3. In XAML, add the converter to your `Binding` ```xml @code { string _graphQLApiStatus = string.Empty; async Task GetGraphQLApiStatus() { var apiRateLimitStatuses = await GitHubApiStatusService.GetApiRateLimits(System.Threading.CancellationToken.None).ConfigureAwait(false); _graphQLApiStatus = apiRateLimitStatuses.GraphQLApi.ToString(); } } ``` ### Apps Using GitHubApiStatus Here are a few open-source apps using GitHubApiStatus: [GitTrends](https://gittrends.com/?ref=codetraveler.io) (Xamarin) GitTrends is an open-source iOS and Android app that provides insights into your GitHub Repos. Quickly and easily review your total Views, Clones and Stars for each and understand the referring sites to your repositories! [GitStatus](https://github.com/brminnick/GitHubApiStatus/tree/main/Src/GitStatus?ref=codetraveler.io) (Xamarin) GitStatus is an open-source iOS and Android app that displays your current rate limit for the GitHub REST API and the GitHub GraphQL API. [GitStatus.Blazor](https://github.com/brminnick/GitHubApiStatus/tree/main/Src/GitStatus.Web?ref=codetraveler.io) (Blazor) GitStatus.Blazor is an open-source website that displays your current rate limit for the GitHub REST API and the GitHub GraphQL API. ### Using Immutable Objects with SQLite-Net URL: https://codetraveler.io/2020/11/11/using-immutable-objects-with-sqlite-net/ Last updated: 2020-11-11T20:58:59.000Z [SQLite-NET](https://www.nuget.org/packages/sqlite-net-pcl/?ref=codetraveler.io) has become the most popular database, especially amongst [Xamarin](https://dotnet.microsoft.com/learn/xamarin/what-is-xamarin?WT.mc%5Fid=mobile-10939-bramin&ref=codetraveler.io) developers, but it hasn't supported Immutable Objects, until now! If you're already familiar with Immutable vs. Mutable objects, feel free to scroll straight to **Immutable Objects + SQLite-net**. ## What is an Immutable Object? An [Immutable Object](https://devblogs.microsoft.com/premier-developer/simple-immutable-objects/?WT.mc%5Fid=mobile-10939-bramin&ref=codetraveler.io) is a object whose values are defined at initialization and cannot be changed. Let's take a look at the example, below. ### Immutable Object ```csharp public class Person { public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; } //Read-only Property, cannot be changed after initialization public string LastName { get; } //Read-only Property, cannot be changed after initialization } ``` This is an immutable object whose values are required at initialization and can never be changed. ```charp var brandon = new Person("Brandon", "Minnick"); // The values of `brandon` can never be changed // I.e. `person.FirstName = "Kim"` generates a compiler error ``` ## Why Immutable Objects? Now, you're probably wondering "Who cares about immutability?" Immutability is super helpful for using [DTOs (Data Transfer Objects)](https://docs.microsoft.com/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5?WT.mc%5Fid=mobile-10939-bramin&ref=codetraveler.io), e.g. objects retrieved from an API or a Database. A pure DTO should always represent the transferred data, i.e. we should never modify a DTO. ## Immutable Objects + SQLite-NET SQLite always required mutable objects (i.e. every property had to have a `public get` and a `public set`) because SQLite uses [Reflection](https://docs.microsoft.com/dotnet/framework/reflection-and-codedom/reflection?WT.mc%5Fid=mobile-10939-bramin&ref=codetraveler.io) to create the objects it retrieves from our database. But recently, Microsoft released [C# 9.0](https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/?WT.mc%5Fid=mobile-10939-bramin&ref=codetraveler.io) which introduced [Init-Only Setters](https://docs.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-9.0/init?WT.mc%5Fid=mobile-10939-bramin&ref=codetraveler.io)! This allows us to define properties that can be set during initialization, AND cannot be changed. In other words init-only setters let us create immutable objects, and EVEN BETTER, they **allow Reflection to create Immutable Objects**! 🎉 ### Immutable Object using Init-Only Setters ```csharp public class Person { public string FirstName { get; init; } //Read-only Property, can be set during initialization, but cannot be changed after initialization public string LastName { get; init; } //Read-only Property, can be set during initialization, but cannot be changed after initialization } ``` This allows properties to be set during initialization AND does not allow the properties to be changed. ```csharp var brandon = new Person { FirstName = "Brandon", LastName = "Minnick" }; // The values of `brandon` can never be changed // I.e. `person.FirstName = "Kim"` now causes a compiler error ``` And the best part is that it is ALSO valid for SQLite because Reflection can still set the properties during initialization!!! ## Example If you're looking for a completed sample, check out this code from [InvestmentDataSampleApp](https://github.com/brminnick/InvestmentDataSampleApp/blob/main/Src/InvestmentDataSampleApp/Models/OpportunityModel.cs?ref=codetraveler.io). ```csharp using SQLite; static class Program { public static async Task Main() { var databasePath = Path.Combine(Xamarin.Essentials.FileSystem.AppDataDirectory, "Database.db3"); var databaseConnection = new SQLiteAsyncConnection(databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache); await databaseConnection.CreateTablesAsync(CreateFlags.None, typeof(PersonModel)); // Save Immutable Object to Database await databaseConnection.InsertOrReplaceAsync(new Person { FirstName = "Brandon", LastName = "Minnick" }); // Save Immutable Object to Database await databaseConnection.InsertOrReplaceAsync(new Person { FirstName = "James", LastName = "Montemagno" }); //Retrieve all immutable obects from Database, placing it into a read-only List (e.g. an immutable List) IReadonlyList allPeople = await databaseConnection.Table().ToListAsync(); } } class PersonModel { [PrimaryKey, AutoIncrement] //PrimaryKey must be nullable: https://github.com/praeclarum/sqlite-net/issues/327 public int? ID { get; init;} public string FirstName { get; init; } public string LastName { get; init; } } ``` ### Adding a Search Bar to Xamarin.Forms.NavigationPage URL: https://codetraveler.io/2020/10/31/adding-a-search-bar-to-xamarin-forms-navigationpage/ Last updated: 2023-08-11T17:08:55.000Z **31 Oct 2020 Update:** Updated for AndroidX + Xamarin.Essentials Let's look at how to add the native search bar control into the `Xamarin.Forms.NavigationPage` Both Xamarin.iOS and Xamarin.Android offer the capability to natively add a search bar to the navigation bar using `UISearchController` and `SearchView`, respectively. But, it's not simple in Xamarin.Forms as one might expect. If you'd like to see a completed solution, check out this app: [](https://github.com/brminnick/GitHubXamarin?ref=codetraveler.io)[https://github.com/brminnick/GitTrends](https://github.com/brminnick/GitTrends?ref=codetraveler.io) Otherwise, let's jump to the steps & code following this lovely GIF: | iOS | Android | | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ![iOS Gif](https://user-images.githubusercontent.com/13558917/62826588-d6c73480-bb83-11e9-8536-ec67e32c5c82.gif) | ![Android Gif](https://user-images.githubusercontent.com/13558917/62826859-e432ed80-bb88-11e9-8418-3ce1cde2dd42.gif) | ## Xamarin.Forms Project In the Xamarin.Forms project, we'll want to do the following: - Use a [Xamarin.Forms Platform Specific](https://docs.microsoft.com/dotnet/api/xamarin.forms.platformconfiguration.iosspecific.navigationpage.setpreferslargetitles?WT.mc%5Fid=navigationbar-codetraveler-bramin&ref=codetraveler.io) to use Large Titles on iOS - Create an interface, `ISearchPage` - In our `Xamarin.Forms.ContentPage`, implement `ISearchPage` ### iOS Large Titles In iOS 11, Apple introduced [prefersLargeTitles](https://developer.apple.com/documentation/uikit/uinavigationbar/2908999-preferslargetitles?ref=codetraveler.io) and updated their [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/ios/bars/navigation-bars/?ref=codetraveler.io), recommending that iOS apps use Large Titles. This is easy to accomplish in Xamarin.Forms thanks to the platform-specific method `[Xamarin.Forms.PlatformConfiguration.iOSSpecific.NavigationPage.SetPreferesLargeTitles](https://docs.microsoft.com/dotnet/api/xamarin.forms.platformconfiguration.iosspecific?view=xamarin-forms?WT.mc%5Fid=navigationbar-codetraveler-bramin&ref=codetraveler.io)` . We can set this platform-specific in our `App.cs` class: ```csharp using Xamarin.Forms.PlatformConfiguration; using Xamarin.Forms.PlatformConfiguration.iOSSpecific; public class App : Xamarin.Forms.Application { public App() { var navigationPage = new Xamarin.Forms.NavigationPage(new MyContentPage()); navigationPage.On().SetPrefersLargeTitles(true); MainPage = navigationPage; } } ``` ### ISearchPage Let's create an interface, `ISearchPage`, that can be used across the Xamarin.Forms, Xamarin.Android and Xamarin.iOS projects. ```csharp public interface ISearchPage { void OnSearchBarTextChanged(string text); event EventHandler SearchBarTextChanged; } ``` ### Xamarin.Forms.ContentPage Let's now implement `ISearchPage` in our `Xamarin.Forms.ContentPage` ```csharp public class MyContentPage : ContentPage, ISearchPage { public MyContentPage() { SearchBarTextChanged += HandleSearchBarTextChanged; } public event EventHandler SearchBarTextChanged; void ISearchPage.OnSearchBarTextChanged((string text) => SearchBarTextChanged?.Invoke(this, text); void HandleSearchBarTextChanged(object sender, string searchBarText) { //Logic to handle updated search bar text } } ``` ## Xamarin.iOS Project In the Xamarin.iOS project, we'll be doing the following: - Create `SearchPageRenderer` Our Xamarin.iOS [Custom Renderer](https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/custom-renderer?WT.mc%5Fid=navigationbar-codetraveler-bramin&ref=codetraveler.io) uses a `PageRenderer` that adds a `UISearchController` to the `NavigationItem` on its parent page which is a `UINavigationController`. (The parent page is a `UINavigationController`, because in `App.cs`, we used a `Xamarin.Forms.NavigationPage`.) If you're new to Xamarin.iOS and/or Custom Renderers, here's a chart showing how the various UI controls relate to each other on iOS: | Xamarin.iOS | Xamarin.Forms Renderer | Xamarin.Forms | | ---------------------- | ---------------------- | -------------- | | UINavigationController | NavigationRenderer | NavigationPage | | UIViewController | PageRenderer | ContentPage | ```csharp using System; using UIKit; using MyNamespace; using MyNamespace.iOS; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; [assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))] namespace MyNamespace.iOS { public class SearchPageRenderer : PageRenderer, IUISearchResultsUpdating { readonly UISearchController _searchController; public SearchPageRenderer() { _searchController = new UISearchController(searchResultsController: null) { SearchResultsUpdater = this, DimsBackgroundDuringPresentation = false, HidesNavigationBarDuringPresentation = false, HidesBottomBarWhenPushed = true }; _searchController.SearchBar.Placeholder = string.Empty; } public override void ViewDidAppear(bool animated) { base.ViewDidAppear(animated); if (ParentViewController.NavigationItem.SearchController is null) { ParentViewController.NavigationItem.SearchController = _searchController; DefinesPresentationContext = true; //Work-around to ensure the SearchController appears when the page first appears https://stackoverflow.com/a/46313164/5953643 ParentViewController.NavigationItem.SearchController.Active = true; ParentViewController.NavigationItem.SearchController.Active = false; } } public override void ViewWillDisappear(bool animated) { base.ViewWillDisappear(animated); ParentViewController.NavigationItem.SearchController = null; } public void UpdateSearchResultsForSearchController(UISearchController searchController) { if (Element is ISearchPage searchPage) searchPage.OnSearchBarTextChanged(searchController.SearchBar.Text); } protected override void OnElementChanged(VisualElementChangedEventArgs e) { base.OnElementChanged(e); if (e.NewElement != null) e.NewElement.SizeChanged += HandleSizeChanged; } //Work-around to accommodate UISearchController height, https://github.com/brminnick/GitTrends/issues/171 void HandleSizeChanged(object sender, EventArgs e) { if (ParentViewController?.NavigationItem.SearchController != null && Element.Height > -1 && Element is Page page) { Element.SizeChanged -= HandleSizeChanged; if (NavigationController.NavigationBar.PrefersLargeTitles is true) { var statusBarSize = UIApplication.SharedApplication.StatusBarFrame.Size; var statusBarHeight = Math.Min(statusBarSize.Height, statusBarSize.Width); page.Padding = new Thickness(page.Padding.Left, page.Padding.Top, page.Padding.Right, page.Padding.Bottom + statusBarHeight); } } } } } ``` **Note**: If you'd like to add a search bar to multiple pages, you can add additional `ExportRenderer` assembly attributes like so: ```csharp [assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))] [assembly: ExportRenderer(typeof(MyContentPage2), typeof(SearchPageRenderer))] ``` ## Xamarin.Android Project In the Xamarin.Android project, we'll be doing the following: - Create `Resources > menu > MainMenu.xml` - Create `SearchPageRenderer` ### MainMenu.xml In the Xamarin.Android project, we'll first need to add `MainMenu.xml` to `Resources > menu`. 1\. In the Xamarin.Android project, in the `Resources` folder, create a new folder called `menu` (if one doesn't already exist) - **Note**: The folder name, `menu`, needs to be lowercase 2\. In the `Resources > menu` folder, create a new file called `MainMenu.xml` ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-10-at-12.30.58-PM.png) 3\. Open `Resources > menu > MainMenu.xml` 4\. In `MainMenu.xml`, add the following code: ```xml ``` ### Xamarin.Android Custom Renderer Our Xamarin.Android [Custom Renderer](https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/custom-renderer?WT.mc%5Fid=navigationbar-codetraveler-bramin&ref=codetraveler.io) uses a `PageRenderer` to add a `SearchView` to the `Toolbar`. - **Note:** Our custom renderer requires the [Xamarin.Essentials NuGet Package](https://www.nuget.org/packages/Xamarin.Essentials/?ref=codetraveler.io). Make sure to add the NuGet package to your Xamarin.Android project first and follow [its instructions to initialize it](https://docs.microsoft.com/xamarin/essentials/get-started?tabs=macos%2Candroid&WT.mc%5Fid=mobile-0000-bramin&ref=codetraveler.io) in the `MainApplication`. ```csharp using System.Collections.Generic; using System.Linq; using Android.Content; using Android.Runtime; using Android.Text; using Android.Views; using Android.Views.InputMethods; using AndroidX.AppCompat.Widget; using MyNamespace; using MyNamespace.Droid; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; [assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))] namespace MyNamespace.Droid { public class SearchPageRenderer : PageRenderer { public SearchPageRenderer(Context context) : base(context) { } //Add the Searchbar once Xamarin.Forms creates the Page protected override void OnElementChanged(ElementChangedEventArgs e) { base.OnElementChanged(e); if (e.NewElement is ISearchPage && e.NewElement is Page page && page.Parent is NavigationPage navigationPage && navigationPage.CurrentPage is ISearchPage) AddSearchToToolbar(page.Title); } protected override void OnAttachedToWindow() { base.OnAttachedToWindow(); if (Element is ISearchPage && Element is Page page && page.Parent is NavigationPage navigationPage) { //Workaround to re-add the SearchView when navigating back to an ISearchPage, because Xamarin.Forms automatically removes it navigationPage.Popped += HandleNavigationPagePopped; navigationPage.PoppedToRoot += HandleNavigationPagePopped; } } //Adding the SearchBar in OnSizeChanged ensures the SearchBar is re-added after the device is rotated, because Xamarin.Forms automatically removes it protected override void OnSizeChanged(int w, int h, int oldw, int oldh) { base.OnSizeChanged(w, h, oldw, oldh); if (Element is ISearchPage && Element is Page page && page.Parent is NavigationPage navigationPage && navigationPage.CurrentPage is ISearchPage) { AddSearchToToolbar(page.Title); } } protected override void Dispose(bool disposing) { if (GetToolbar() is Toolbar toolBar) toolBar.Menu?.RemoveItem(Resource.Menu.MainMenu); base.Dispose(disposing); } static IEnumerable GetToolbars(ViewGroup viewGroup) { for (int i = 0; i < viewGroup.ChildCount; i++) { if (viewGroup.GetChildAt(i) is Toolbar toolbar) { yield return toolbar; } else if (viewGroup.GetChildAt(i) is ViewGroup childViewGroup) { foreach (var childToolbar in GetToolbars(childViewGroup)) yield return childToolbar; } } } Toolbar? GetToolbar() { if (Xamarin.Essentials.Platform.CurrentActivity.Window?.DecorView.RootView is ViewGroup viewGroup) { var toolbars = GetToolbars(viewGroup); //Return top-most Toolbar return toolbars.LastOrDefault(); } return null; } //Workaround to re-add the SearchView when navigating back to an ISearchPage, because Xamarin.Forms automatically removes it void HandleNavigationPagePopped(object sender, NavigationEventArgs e) { if (sender is NavigationPage navigationPage && navigationPage.CurrentPage is ISearchPage) { AddSearchToToolbar(navigationPage.CurrentPage.Title); } } void AddSearchToToolbar(string pageTitle) { if (GetToolbar() is Toolbar toolBar && toolBar.Menu?.FindItem(Resource.Id.ActionSearch)?.ActionView?.JavaCast()?.GetType() != typeof(SearchView)) { toolBar.Title = pageTitle; toolBar.InflateMenu(Resource.Menu.MainMenu); if (toolBar.Menu?.FindItem(Resource.Id.ActionSearch)?.ActionView?.JavaCast() is SearchView searchView) { searchView.QueryTextChange += HandleQueryTextChange; searchView.ImeOptions = (int)ImeAction.Search; searchView.InputType = (int)InputTypes.TextVariationFilter; searchView.MaxWidth = int.MaxValue; //Set to full width - http://stackoverflow.com/questions/31456102/searchview-doesnt-expand-full-width } } } void HandleQueryTextChange(object sender, SearchView.QueryTextChangeEventArgs e) { if (Element is ISearchPage searchPage) searchPage.OnSearchBarTextChanged(e?.NewText ?? string.Empty); } } } ``` **Note**: If you'd like to add a search bar to multiple pages, you can add additional `ExportRenderer` assembly attributes like so: ```csharp [assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))] [assembly: ExportRenderer(typeof(MyContentPage2), typeof(SearchPageRenderer))] ``` ### Improving CollectionView Scrolling URL: https://codetraveler.io/2020/07/12/improving-collectionview-scrolling/ Last updated: 2020-07-17T16:31:11.000Z ## Background I was first [alerted by @anaseeen on Twitter](https://twitter.com/anaseeen/status/1281610967336595458?s=20&ref=codetraveler.io) that my [GitTrends app](https://gittrends.com/?ref=codetraveler.io) had substantial jitter when scrolling the `CollectionView`. I verified the jitter was also happening on my Google Pixel 3\. Because the Pixel 3 has substantial CPU and RAM resources, this test confirmed that this problem is not device-specific and there is a performance issue in my code. ## Solution I found that two things were causing the UI to jitter when the user scrolls a [CollectionView](https://docs.microsoft.com/xamarin/xamarin-forms/user-interface/collectionview/?WT.mc%5Fid=ImprovingCollectionView-codetraveler-bramin&ref=codetraveler.io): - Bindings - Garbage Collection I was able to remove the jitter by removing bindings from my `DataTemplate` and increasing the size of the [Android Nursery](https://docs.microsoft.com/xamarin/android/internals/garbage-collection?WT.mc%5Fid=ImprovingCollectionView-codetraveler-bramin&ref=codetraveler.io#configuration) which decreases the frequency of garbage collections. If you'd like to see exactly how I solved it, here is the Pull Request that fixed it in my [GitTrends app](https://gittrends.com/?ref=codetraveler.io): [https://github.com/brminnick/GitTrends/pull/143](https://github.com/brminnick/GitTrends/pull/143?ref=codetraveler.io) | Before (Substantial Scrolling Jitter) | After (No Scrolling Jitter) | | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | ![BeforeFix](https://user-images.githubusercontent.com/13558917/87251186-9a005f00-c41e-11ea-90e7-d3ac9d58e56d.gif) | ![AfterFix](https://user-images.githubusercontent.com/13558917/87251189-a5ec2100-c41e-11ea-86f8-089a9d1ce616.gif) | ## Removing DataTemplate Bindings To remove bindings, we can use a `[DataTemplateSelector](https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/templates/data-templates/selector?WT.mc%5Fid=ImprovingCollectionView-codetraveler-bramin&ref=codetraveler.io)` to pass the `BindingContext` directly into our `[DataTemplate](https://docs.microsoft.com/dotnet/api/xamarin.forms.datatemplate?WT.mc%5Fid=ImprovingCollectionView-codetraveler-bramin&ref=codetraveler.io)` and assign the values on initialization. ### Before Removing DataTemplate Bindings ```csharp using Xamarin.Forms.Markup; class CollectionViewPage : ContentPage { Content = new CollectionView { ItemTemplate = new ImageDataTemplate() }.Bind(CollectionView.ItemsSourceProperty, nameof(CollectionViewModel.ImageList)); } class ImageModel { public string ImageTitle { get; set; } public string ImageUrl { get; set; } } class ImageDataTemplate : DataTemplate { public MyDataTemplate() : base(() => CreateDataTemplate()) { } static View CreateDataTemplate() => new StackLayout { Children = { new Image().Bind(Image.SourceProperty, nameof(ImageModel.ImageUrl)) new Label().Bind(Label.TextProperty, nameof(ImageModel.ImageTitle)) } } } ``` ### After Removing DataTemplate Bindings ```csharp using Xamarin.Forms.Markup; class CollectionViewPage : ContentPage { Content = new CollectionView { ItemTemplate = new ImageDataTemplateSelector() }.Bind(CollectionView.ItemsSourceProperty, nameof(CollectionViewModel.ImageList)); } class ImageModel { public string ImageTitle { get; set; } public string ImageUrl { get; set; } } class ImageDataTemplateSelector : DataTemplateSelector { protected override DataTemplate OnSelectTemplate(object item, BindableObject container) => new ImageDataTemplate((ImageModel)item); class ImageDataTemplate : DataTemplate { public MyDataTemplate(ImageModel imageModel) : base(() => CreateDataTemplate(imageModel)) { } static View CreateDataTemplate(ImageModel imageModel) => new StackLayout { Children = { new Image { Source = imageModel.ImageUrl }, new Label { Text = imageModel.ImageTitle } } } } } ``` ## Increase Nursery Size to Decrease Garbage Collection We can [set the size of the Android Nursery by setting it in the MONO\_GC\_PARAMS](https://docs.microsoft.com/xamarin/android/internals/garbage-collection?WT.mc%5Fid=ImprovingCollectionView-codetraveler-bramin&ref=codetraveler.io#configuration): 1\. In the Xamarin.Android project, create a new file called `GarbageCollector.config` 2\. In `GarbageCollector.config`, add the following line of code: ```bash MONO_GC_PARAMS=nursery-size=64m ``` > **Note:** The value for `nursery-size` must be a power of 2 (e.g. 2, 4, 8, 16, 32, 64, 128 etc) > **Note:** I recommend trying different values for your `nursery-size` because each app is different and will have different memory requirements 3\. In Visual Studio, in the Solution Explorer, right-click on `GarbageCollector.config` 4\. In the right-click menu, select **BuildAction** \> **AndroidEnvironment** ![Screen Shot 2020-07-12 at 8 53 36 AM](https://user-images.githubusercontent.com/13558917/87250963-388bc080-c41d-11ea-996b-6c16cd718646.png) ### Introducing GitTrends: GitHub Insights URL: https://codetraveler.io/2020/03/02/introducing-gittrends/ Last updated: 2020-03-03T02:55:16.000Z | iOS | Android | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [![](https://user-images.githubusercontent.com/13558917/75712535-ed96bb00-5c7c-11ea-8bd4-e9bd36365bb2.png)](https://apps.apple.com/us/app/gittrends-github-insights/id1500300399?ref=codetraveler.io) | [![](https://user-images.githubusercontent.com/13558917/75712286-8d077e00-5c7c-11ea-86f0-cd693630eee0.png)](https://play.google.com/store/apps/details?id=com.minnick.gittrends&ref=codetraveler.io) | I am so excited to release GitTrends on Product Hunt today! [https://www.producthunt.com/posts/gittrends-github-insights](https://www.producthunt.com/posts/gittrends-github-insights?ref=codetraveler.io) GitTrends helps you to monitor your GitHub Repositories: ✅ Connect to GitHub ✅ Monitor GitHub Repo Views ✅ Monitor GitHub Repo Clones ✅ Discover Referring Sites I created GitTrends because I love contributing to open source libraries, but I had trouble keeping track of which repos were receiving traffic and which ones were not. For example, I recently discovered that a repo I hadn't updated in +2 years was still getting 10-20 views per week. All of the code is totally open source on GitHub and I absolutely welcome new feature requests and PRs! 📝 [Leave us a comment on Product Hunt](https://www.producthunt.com/posts/gittrends-github-insights?ref=codetraveler.io) ⭐️ [Open a Feature Request on GitHub](https://github.com/brminnick/GitTrends/?ref=codetraveler.io) | GitHub Login | Repo Traffic | Referring Sites | | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | ![View Repositories](https://user-images.githubusercontent.com/13558917/75208427-77500100-5730-11ea-9654-7255ed051536.gif) | ![View Repo](https://user-images.githubusercontent.com/13558917/75208368-5091ca80-5730-11ea-8602-8c63244bf229.gif) | ![Referring Site](https://user-images.githubusercontent.com/13558917/75208328-30faa200-5730-11ea-92c7-d316f6cb39e6.gif) | | ![View Repositories](https://user-images.githubusercontent.com/13558917/74350902-43a5cc00-4d6b-11ea-8c9f-c1bafbfcb415.gif) | ![View Repo](https://user-images.githubusercontent.com/13558917/74351618-363d1180-4d6c-11ea-8244-dc2e1c6cdfe4.gif) | ![Referring Site](https://user-images.githubusercontent.com/13558917/74351991-b9f6fe00-4d6c-11ea-9eb5-57260493256b.gif) | ## Resources ### Mobile App - [Xamarin](https://docs.microsoft.com/xamarin/?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- build native apps for Android, iOS, and macOS using .NET code and platform-specific user interfaces - [Xamarin.Essentials](https://docs.microsoft.com/xamarin/essentials/?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- provides cross-platform APIs for their mobile applications - [Xamarin.Forms](https://docs.microsoft.com/xamarin/get-started/what-is-xamarin-forms?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- an open-source UI framework to build Android, iOS, and Windows applications from a single shared codebase - [AppCenter](https://docs.microsoft.com/appcenter/?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- continuously build, test, release, and monitor mobile apps - [AsyncAwaitBestPractices](https://github.com/brminnick/AsyncAwaitBestPractices?ref=codetraveler.io) \- Extensions for System.Threading.Tasks.Task - [Autofac](https://github.com/autofac/Autofac?ref=codetraveler.io) \- an IoC container for .NET - [Debug Rainbows](https://github.com/sthewissen/Xamarin.Forms.DebugRainbows?ref=codetraveler.io) \- Xamarin.Forms UI layout tool - [HTML Agility Pack](https://github.com/zzzprojects/html-agility-pack?ref=codetraveler.io) \- an agile HTML parser that builds a read/write DOM and supports plain XPATH or XSLT - [Polly](https://github.com/App-vNext/Polly?ref=codetraveler.io) \- .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner - [Refit](https://github.com/reactiveui/refit?ref=codetraveler.io) \- Refit is a REST library for .NET that converts REST APIs into live interfaces - [Sqlite-net](https://github.com/praeclarum/sqlite-net?ref=codetraveler.io) \- an open source, minimal library to allow .NET, .NET Core, and Mono applications to store data in SQLite 3 databases - [Syncfusion](https://syncfusion.com/?ref=codetraveler.io) \- a UI Component Suite for Building Powerful Web, Desktop, and Mobile Apps ### Cloud Backend - [.NET Core](https://docs.microsoft.com/dotnet/core/?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- an [open-source](https://github.com/dotnet/core?ref=codetraveler.io), general-purpose development platform maintained by Microsoft and the .NET community on [GitHub](https://github.com/dotnet/core?ref=codetraveler.io) - [Azure Functions](https://docs.microsoft.com/azure/azure-functions/?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- a serverless compute service that lets you run event-triggered code without having to explicitly provision or manage infrastructure - [Azure Functions Dependency Injection](https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- Azure Functions supports the dependency injection (DI) software design pattern, which is a technique to achieve [Inversion of Control (IoC)](https://docs.microsoft.com/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io#dependency-inversion) between classes and their dependencies - [Polly + HttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io) \- the recommended approach for retries with exponential backoff is to take advantage of more advanced .NET libraries - [Refit + HttpClientFactory](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?WT.mc%5Fid=gittrends-codetraveler-bramin&ref=codetraveler.io#generated-clients) \- `IHttpClientFactory` can be used in combination with third-party libraries such as Refit ### Updating to Azure Functions v3 in Visual Studio URL: https://codetraveler.io/2020/02/12/visual-studio-for-mac-updating-to-azure-functions-v3/ Last updated: 2020-06-30T18:58:03.000Z Azure Functions v3 was recently [released on 09 December 2019](https://azure.microsoft.com/updates/announcing-go-live-release-for-azure-functions-v3?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io) with support for [.NET Core 3](https://docs.microsoft.com/dotnet/core/whats-new/dotnet-core-3-0?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io) and [.NET Standard 2.1](https://devblogs.microsoft.com/dotnet/announcing-net-standard-2-1?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io). Here's how to install the new runtime in Visual Studio and Visual Studio for Mac. ## 1\. Install .NET Core 3 Azure Functions v3 runs on .NET Core 3\. To install .NET Core 3, visit [Download .NET Core](https://dotnet.microsoft.com/download/dotnet-core?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io). I recommend selecting the latest `LTS` version. `LTS` stands for `Long Term Support`, meaning that Microsoft is committed to supporting this specific version of .NET Core with bug fixes for approximately 2-3 years. As of today, the current `LTS` version of .NET Core is [.NET Core 3.1](https://dotnet.microsoft.com/download/dotnet-core/3.1?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io). ## 2\. Update the CSPROJ Let's ensure our `csproj` file has been updated for Azure Functions v3\. We'll need to set the following three things: - [**Target Framework**](https://docs.microsoft.com/dotnet/standard/frameworks?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io) is `netcoreapp3.1` (or higher) - [**Microsoft.NET.Sdk.Functions** NuGet Package](https://www.nuget.org/packages/Microsoft.NET.Sdk.Functions?ref=codetraveler.io) is v3.0.1 (or higher) - **AzureFunctionsVersion** is `v3` Here is an example from my GitTrends app: [https://github.com/brminnick/GitTrends/blob/master/GitTrends.Functions/GitTrends.Functions.csproj](https://github.com/brminnick/GitTrends/blob/master/GitTrends.Functions/GitTrends.Functions.csproj?ref=codetraveler.io) ```xml netcoreapp3.1 v3 PreserveNewest PreserveNewest Never ``` ## 3a. (Visual Studio) Update Azure Functions Runtime **Note:** If you're using Visual Studio for Mac, skip to the next section Let's now install the Azure Functions Runtime for [Visual Studio 2019](https://visualstudio.microsoft.com/vs?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io) 1. In [**Visual Studio**](https://visualstudio.microsoft.com/vs?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io), select **Create a new project** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/Screen-Shot-2020-02-12-at-3.31.20-PM.png) Visual Studio, Create New Project 2\. In the **Create a new project** window, in the search bar, enter `Functions` 3\. In the **Create a new project** window, in the search results, select **Azure Functions** 4\. In the **Create a new project** window, select **Next** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/Screen-Shot-2020-02-12-at-3.31.35-PM.png) Create a new Azure Functions Project 5\. In the **Create a new Azure Functions Application** window, stand by while it is **"Getting information about the latest function tools..."** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/Screen-Shot-2020-02-12-at-3.31.53-PM.png) Download latest Azure Functions Runtime 6\. In the **Create a new Azure Functions Application** window, once the new tools have been downloaded, click **Refresh** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/Screen-Shot-2020-02-12-at-3.33.24-PM.png) Click Refresh ## 3b. (Visual Studio for Mac) Update Azure Functions Runtime **Note:** If you are using Visual Studio on PC, you may skip this step Let's now install the Azure Functions Runtime for [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac?WT.mc%5Fid=functionsv3-codetraveler-bramin&ref=codetraveler.io) 1. In the [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/?ref=codetraveler.io) window, select **New** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/Screen-Shot-2020-02-12-at-3.42.44-PM.png) Visual Studio for Mac Create new Project 2\. In the **New Project** window, on the left-hand menu, under **Cloud**, select **General** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/Screen-Shot-2020-02-12-at-2.23.18-PM.png) Create New Azure Functions Project 3\. In the **Configure you Azure Functions Project** window, standby until it finishes installing the Azure Functions components ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/Screen-Shot-2020-02-12-at-2.23.27-PM.png) Update Runtime ## 4\. Conclusion Updating to Azure Functions v3 requires a couple steps: - Installing .NET Core 3 - Updating our `csproj` - Updating Visual Studio's Azure Functions Runtime If you'd like to see an existing Azure Functions project using v3, feel free to check the Azure Functions Backend in my GitTrends app: [https://github.com/brminnick/GitTrends/tree/master/GitTrends.Functions](https://github.com/brminnick/GitTrends/tree/master/GitTrends.Functions?ref=codetraveler.io) ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2020/02/EQm9wkaUYAADq2V-1.jpeg) ### Xamarin: Efficiently Using a SQLite Database URL: https://codetraveler.io/2019/11/26/efficiently-initializing-sqlite-database/ Last updated: 2022-10-14T23:57:56.000Z The most popular SQLite ORM for Xamarin is [SQLite-net](https://github.com/praeclarum/sqlite-net?ref=codetraveler.io). Let's explore the most efficient way to use it in our apps! If you'd like to skip to the completed code, jump to Section 9: *Put It All Together*. A completed sample app using this implementation is also available here: [https://github.com/brminnick/GitTrends](https://github.com/brminnick/GitTrends?ref=codetraveler.io) ## 1\. Locate the App Data Directory Both iOS and Android have a specific/unique file directory for each app, and this is where we'll store the `.db3` database file. We can easily locate it using [Xamarin.Essentials.FileSystem](https://docs.microsoft.com/xamarin/essentials/file-system-helpers?WT.mc%5Fid=sqlite-codetraveler-bramin&ref=codetraveler.io#using-file-system-helpers). **Note**: Be sure to first add the [Xamarin.Essentials NuGet Package](https://www.nuget.org/packages/Xamarin.Essentials?ref=codetraveler.io) and follow its [Getting Started Instructions](https://docs.microsoft.com/xamarin/essentials/get-started?WT.mc%5Fid=sqlite-codetraveler-bramin&ref=codetraveler.io). ```csharp static readonly string _databasePath = Path.Combine(Xamarin.Essentials.FileSystem.AppDataDirectory, "SqliteDatabase.db3"); ``` ## 2\. Use SQLiteOpenFlags SQLite-net offers an [asynchronous API](https://github.com/praeclarum/sqlite-net/wiki/Asynchronous-API?ref=codetraveler.io), `SQLiteAsyncConnection`. This allows us to use `async/await` when performing database operations. SQLite also offers a [few different options](https://www.sqlite.org/c3ref/open.html?ref=codetraveler.io) when creating a database connection using `SQLiteOpenFlags`, and here are the ones I recommend: - [SQLiteOpenFlags.ReadWrite](https://www.sqlite.org/c3ref/open.html?ref=codetraveler.io) - > "The database is opened for reading and writing if possible, or reading only if the file is write protected by the operating system" - [SQLiteOpenFlags.Create](https://www.sqlite.org/c3ref/open.html?ref=codetraveler.io) - > "\[The database\] is created if it does not already exist" - [SQLiteOpenFlags.SharedCache](https://www.sqlite.org/sharedcache.html?ref=codetraveler.io) - Allows for multi-threaded database access - > "When a thread establishes multiple connections to the same database, the connections share a single data and schema cache. This can significantly reduce the quantity of memory and IO required by the system" ```csharp new SQLiteAsyncConnection(_databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache) ``` ## 3\. Use Lazy Initialization The [official Microsoft Docs](https://docs.microsoft.com/dotnet/framework/performance/lazy-initialization?WT.mc%5Fid=sqlite-codetraveler-bramin&ref=codetraveler.io) say it best: > Lazy initialization of an object means that its creation is deferred until it is first used. Because File IO operations and creating a database can be expensive (i.e., it requires many CPU cycles and can take longer than expected), we don't want to initialize our database until we need it. This avoids creating our database when the app launches, keeping our app's launch time to a minimum. ```csharp static readonly Lazy _databaseConnectionHolder = new Lazy(() => new SQLiteAsyncConnection(_databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache)); static SQLiteAsyncConnection DatabaseConnection => _databaseConnectionHolder.Value; ``` ## 4\. Retrieve the Database Connection To use the database connection, we must ensure the following: 1. Ensure Database has been created - `new SQLiteAsyncConnection` 2. Ensure Tables have been created & mapped - `CreateTables(CreateFlags createFlags, params Type[] types)` To accomplish both, I created `async Task GetDatabaseConnection()` where `T` is the Model's type. This method ensures the database table has been created & mapped before returning the database connection. ```csharp protected static async Task GetDatabaseConnection() { if (!DatabaseConnection.TableMappings.Any(static x => x.MappedType == typeof(T))) { await DatabaseConnection.CreateTablesAsync(CreateFlags.None, typeof(T)).ConfigureAwait(false); } return DatabaseConnection; } ``` ## 5\. Use Write-Ahead Logging In [SQLite-net v1.6.0+](https://github.com/praeclarum/sqlite-net/blob/91c17adc183487dd3e7a41e90430c286a7add7db/src/SQLiteAsync.cs?ref=codetraveler.io#L128), Write Ahead Logging (WAL), is available [which brings many performance advantages](https://www.sqlite.org/wal.html?ref=codetraveler.io): > 1. WAL is significantly faster in most scenarios > 2. WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently. > 3. Disk I/O operations tends to be more sequential using WAL. > 4. WAL uses many fewer fsync() operations and is thus less vulnerable to problems on systems where the fsync() system call is broken. There are also [disadvantages to using WAL](https://www.sqlite.org/wal.html?ref=codetraveler.io), most of which however don't impact Xamarin mobile apps. But be sure to understand both the advantages and disadvantages before enabling WAL in your app. ```csharp protected static async Task GetDatabaseConnection() { if (!DatabaseConnection.TableMappings.Any(static x => x.MappedType == typeof(T))) { // On sqlite-net v1.6.0+, enabling write-ahead logging allows for faster database execution await DatabaseConnection.EnableWriteAheadLoggingAsync().ConfigureAwait(false); await DatabaseConnection.CreateTablesAsync(CreateFlags.None, typeof(T)).ConfigureAwait(false); } return DatabaseConnection; } ``` ## 6\. Always Attempt and Retry When using multi-threading and async/await to interact with a SQLite database, sometimes this exception is thrown: > SQLite.SQLiteException: database is locked Anecdotally, I've noticed this exception gets thrown more often in SQLite-net v1.6.0+ when WAL is enabled. When this exception happens, we just need to retry our SQL query again after the database has been unlocked. There is an amazing library called [Polly](https://www.nuget.org/packages/Polly/?ref=codetraveler.io) that will catch an exception, wait a specified period of time, then try to execute the function again. For this, I created `AttemptAndRetry` which will execute a `Func>`, but if it throws a `SQLite.SQLiteException` it will attempt to execute it again asynchronously. Between retries, I recommend using an [Exponential Backoff](https://wikipedia.org/wiki/Exponential%5Fbackoff?ref=codetraveler.io) to ensure there are 2n milliseconds in between. This means that if the first attempt fails, the first retry will happen 21 (2) milliseconds later (1 millisecond is a *really* long time for a CPU). If the second attempt fails, its subsequent retry will happen 22 (4) milliseconds later. And if third attempt fails, its subsequent retry will happen 23 (8) milliseconds later, etc. In this example, we will retry 10 times, meaning the final attempt will occur 2 seconds after its previous try, and all 11 attempts (1 attempt + 10 retries) will occur over a total timespan of 4.094 seconds. In other words, if each attempt fails, including the final attempt, the database will throw a `SQLite.SQLiteException` after approximately 4 seconds. ```csharp protected static Task AttemptAndRetry(Func> action, int numRetries = 10) { return Policy.Handle().WaitAndRetryAsync(numRetries, pollyRetryAttempt).ExecuteAsync(action); static TimeSpan pollyRetryAttempt(int attemptNumber) => TimeSpan.FromMilliseconds(Math.Pow(2, attemptNumber)); } ``` ## 7\. Use ConfigureAwait(false) You may notice that every time I use the `await` keyword, I also use `.ConfigureAwait(false)`. This is done to increase performance. `ConfigureAwait(false)` ensures that the background thread doesn't return to the calling thread (which can be the Main Thread, aka the UI Thread). For Xamarin apps, we want to avoid using the Main Thread for database operations to ensure our app doesn't freeze and remains responsive. I highly recommend watching this session I delivered at NDC Oslo to learn more: "[Correcting Common Async Await Mistakes in .NET](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io)". [![](https://user-images.githubusercontent.com/13558917/62509892-a32a8a00-b7d2-11e9-9460-267220838b76.png)](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io "Correcting Common Async Await Mistakes in .NET") ## 8\. Use ValueTask New in .NET Standard 2.1 is `[System.Threading.Tasks.ValueTask](https://docs.microsoft.com/dotnet/api/system.threading.tasks.valuetask-1?view=netcore-3.0WT.mc%5Fid=sqlite-codetraveler-bramin&ref=codetraveler.io)`. (If you can't yet upgrade to .NET Standard 2.1, you can still use `ValueTask` by installing the `[System.Threading.Tasks.Extensions](https://www.nuget.org/packages/System.Threading.Tasks.Extensions/?ref=codetraveler.io)` NuGet Package.) If you're new to `ValueTask`, check out this great write-up: [Understanding the Whys, Whats, and Whens of ValueTask](https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask?WT.mc%5Fid=sqlite-codetraveler-bramin&ref=codetraveler.io). **tl;dr** `VaueTask` can be used to improve async/await performance when the hot-path of a method doesn't require the `await` keyword. `GetDatabaseConnection` only uses the `await` keyword the first time it is called. Thus its hot-path does not use require `await`, making it the perfect use-case for `ValueTask`. To implement `ValueTask` for `GetDatabaseConnection`, just replace `Task` with `ValueTask`: ```csharp protected static async ValueTask GetDatabaseConnection() { if (!DatabaseConnection.TableMappings.Any(static x => x.MappedType == typeof(T))) { // On sqlite-net v1.6.0+, enabling write-ahead logging allows for faster database execution await DatabaseConnection.EnableWriteAheadLoggingAsync().ConfigureAwait(false); await DatabaseConnection.CreateTablesAsync(CreateFlags.None, typeof(T)).ConfigureAwait(false); } return DatabaseConnection; } ``` # 9\. Put It All Together Putting all of these best practices together, I've created `abstract class BaseDatabase`. I copy/paste this abstract class into all of my Xamarin applications where I use a database. A completed sample app using this implementation is available here: [https://github.com/brminnick/GitTrends](https://github.com/brminnick/GitTrends?ref=codetraveler.io) ### BaseDatabase.cs Be sure to first add the following NuGet Packages to your app: - [SQLite-Net](https://www.nuget.org/packages/sqlite-net-pcl/?ref=codetraveler.io) - [Xamarin.Essentials](https://www.nuget.org/packages/Xamarin.Essentials?ref=codetraveler.io) - Used to locate the app's data Directory - [Getting Started Instructions](https://docs.microsoft.com/xamarin/essentials/get-started?WT.mc%5Fid=none-stackoverflow-bramin&ref=codetraveler.io) - [Polly](https://www.nuget.org/packages/Polly/?ref=codetraveler.io) - Used to attempt database actions and automatically retry if unsuccessful - (Polly doesn't require any initialization code) - [ValueTask](https://www.nuget.org/packages/System.Threading.Tasks.Extensions/?ref=codetraveler.io) (Not required for .NET Standard 2.1+) ```csharp using System.Threading.Tasks; using SQLite; using Xamarin.Essentials; using Xamarin.Forms; namespace MyNamespace { public abstract class BaseDatabase { static readonly string _databasePath = Path.Combine(FileSystem.AppDataDirectory, "SqliteDatabase.db3"); static readonly Lazy _databaseConnectionHolder = new Lazy(() => new SQLiteAsyncConnection(_databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache)); static SQLiteAsyncConnection DatabaseConnection => _databaseConnectionHolder.Value; protected static async ValueTask GetDatabaseConnection() { if (!DatabaseConnection.TableMappings.Any(static x => x.MappedType == typeof(T))) { // On sqlite-net v1.6.0+, enabling write-ahead logging allows for faster database execution await DatabaseConnection.EnableWriteAheadLoggingAsync().ConfigureAwait(false); await DatabaseConnection.CreateTablesAsync(CreateFlags.None, typeof(T)).ConfigureAwait(false); } return DatabaseConnection; } protected static Task AttemptAndRetry(Func> action, int numRetries = 10) { return Policy.Handle().WaitAndRetryAsync(numRetries, pollyRetryAttempt).ExecuteAsync(action); static TimeSpan pollyRetryAttempt(int attemptNumber) => TimeSpan.FromMilliseconds(Math.Pow(2, attemptNumber)); } } } ``` ### Example: Inheriting from BaseDatabase ```csharp namespace MyNamespace { public class CustomerDatabase : BaseDatabase { public async Task> GetAllCustomers() { var databaseConnection = await GetDatabaseConnection().ConfigureAwait(false); return await AttemptAndRetry(() => databaseConnection.Table().ToListAsync()).ConfigureAwait(false); } } } ``` ## Conclusion I highly recommend using `BaseDatabase.cs` in all of your Xamarin apps to take advantage of these awesome performance improvements! - **[Lazy Loading](https://docs.microsoft.com/dotnet/framework/performance/lazy-initialization?WT.mc%5Fid=sqlite-codetraveler-bramin&ref=codetraveler.io)** \- ensure your app launches quickly - **[Write Ahead Logging](https://www.sqlite.org/wal.html?ref=codetraveler.io)** \- faster/more-efficient database writes - **Attempt And Retry** \- use [Polly](https://www.nuget.org/packages/Polly/?ref=codetraveler.io) to retry database writes - **[ConfigureAwait(false)](https://www.youtube.com/watch?v=J0mcYVxJEl0&ref=codetraveler.io)** \- avoid unnecessarily returning to the Main Thread - **[ValueTask](https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask?WT.mc%5Fid=sqlite-codetraveler-bramin&ref=codetraveler.io)** \- avoid unnecessary memory allocations ### Xamarin.UITest: Is a ListView (or RefreshView) Refreshing? URL: https://codetraveler.io/2019/11/05/xamarin-uitest-determine-if-xamarin-forms-listview-is-refreshing/ Last updated: 2019-11-05T19:27:52.000Z Automated UI testing has become critical for publishing 5-star apps and many Xamarin.Forms apps display data in a ListView or a RefreshView. But how can our UI tests determine if our list is refreshing? Let's find out! We'll need to access the platform-specific APIs to check if the list is refreshing. Thus, we'll be using different code to test Android vs iOS. ## Testing on Android To interact with the ListView and the RefreshView on Android, we must use the [Invoke](https://docs.microsoft.com/dotnet/api/Xamarin.UITest.Queries.AppQuery.Invoke?view=xamarin-uitest-sdk?WT.mc%5Fid=listviewrefreshing-codetraveler-bramin&ref=codetraveler.io) method to access the [native Java Android API methods](https://developer.android.com/reference/android/support/v4/widget/SwipeRefreshLayout.html?ref=codetraveler.io#isRefreshing%28%29). On Android, we can check the value returned from `SwipeRefreshLayout.isRefreshing()` like so: ```csharp (bool)app.Query(x => x.Class("ListViewRenderer_SwipeRefreshLayoutWithFixedNestedScrolling").Invoke("isRefreshing")).First(); ``` #### RefreshView ```csharp (bool)app.Query(x => x.Class("RefreshViewRenderer").Invoke("isRefreshing")).First(); ``` ## Testing on iOS For iOS, we can check to see if the [UIRefreshControl](https://developer.apple.com/documentation/uikit/uirefreshcontrol?ref=codetraveler.io) is visible: ```csharp app.Query(x => x.Class("UIRefreshControl")).Any() ``` ## Xamarin.UITest Sample ```csharp //Xamarin.Forms.ListView public bool IsListViewRefreshIndicatorDisplayed(Xamarin.UITest.IApp app) { if (app is AndroidApp) return (bool)app.Query(x => x.Class("ListViewRenderer_SwipeRefreshLayoutWithFixedNestedScrolling").Invoke("isRefreshing")).First(); if (app is iOSApp) return app.Query(x => x.Class("UIRefreshControl")).Any(); throw new NotSupportedException("Xamarin.UITest only supports Android and iOS"); } //Xamarin.Forms.RefreshView public bool IsRefreshViewRefreshIndicatorDisplayed(Xamarin.UITest.IApp app) { if (app is AndroidApp) return (bool)app.Query(x => x.Class("RefreshViewRenderer").Invoke("isRefreshing")).First(); if (app is iOSApp) return app.Query(x => x.Class("UIRefreshControl")).Any(); throw new NotSupportedException("Xamarin.UITest only supports Android and iOS"); } ``` ## Sample App Putting it all together, here is a sample app that implements this code: [https://github.com/brminnick/UITestSampleApp/](https://github.com/brminnick/UITestSampleApp/blob/master/Src/UITestSampleApp.UITests/Pages/ListPage.cs?ref=codetraveler.io) ### Xamarin: Creating a Dark Mode Splash Screen URL: https://codetraveler.io/2019/10/11/creating-a-dark-mode-splash-screen/ Last updated: 2019-10-21T14:08:29.000Z Now that [iOS 13](https://docs.microsoft.com/xamarin/ios/platform/ios13/dark-mode?ref=codetraveler.io) and Android Q support Dark Mode, we'll want our app's Splash Screen to be dark too. Let's explore how to create a Splash Screen that is dark when Dark Mode is enabled, but still light when Dark Mode is disabled! Because splash screens appear before the app has initialized, we cannot write any code to determine whether or not the user has enabled Dark Mode. Luckily, both iOS and Android have built-in tools we'll leverage. For our Splash Screen, we will leverage iOS's **Asset Catalogue** and Android's **Resources** \> **values-night** to auto-import our light/dark image assets. I recently implemented this functionality for my app, [Punday](http://mondaypunday.com/app?ref=codetraveler.io), on [iOS](https://appsto.re/us/uYHSab.i?ref=codetraveler.io) and [Android](https://play.google.com/store/apps/details?id=com.minnick.mondaypundayapp&ref=codetraveler.io). Since Punday is not open-source, I put together this walkthrough and a completed sample demonstrating Punday's splash screen. You can find the completed solution here: [https://github.com/brminnick/DarkModeSplashScreen](https://github.com/brminnick/DarkModeSplashScreen?ref=codetraveler.io) ## iOS Splash Screen We'll create our iOS Splash Screen using Storyboards that displays an image on a blank background. To support Dark Mode, we'll add 2 versions of our image into our Asset Catalog, one for dark mode and one for light mode. | Dark Appearance Enabled | Dark Appearance Disabled | | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | ![iOS Splash Screen, Dark Appearance Enabled](https://user-images.githubusercontent.com/13558917/66689821-761aad80-ec41-11e9-8f39-92170e5e8c16.png) | ![iOS Splash Screen, Dark Appearance Disabled](https://user-images.githubusercontent.com/13558917/66689819-761aad80-ec41-11e9-93e8-7cd7addac515.png) | ### 1\. Create SplashScreen.storyboard First, let's create a storyboard that we will use for our iOS splash screen. In our example, our storyboard will contain just a single image. 1. In the **Visual Studio Solution Explorer**, right-click on the Xamarin.iOS project 2. In the right-click menu, select **Add** \> **New File** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-9.44.34-AM.png) Xamarin.iOS: Add New File 3\. In the **New File** window, select **iOS** \> **Storyboard** 4\. In the **New File** window, name the new file **SplashScreen** 5\. In the **New File** window, select **New** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-9.44.46-AM.png) Create SplashScreen.storyboard 6\. In the **Solution Explorer**, open **SplashScreen.storyboard** 7\. In the **SplashScreen.storyboard Editor**, onto the storyboard canvas, drag-and-drop a new **View Controller** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-9.54.29-AM.png) Add New UIViewController 8\. In the **SplashScreen.storyboard Editor**, click on the View Controller to select it 9\. In the **Properties** window, select **Widget** 10\. In the **Widget** pane, set the **Background** to **System Background Color** > Note: **System Background Color** will be white when the user has not enabled Dark Theme in iOS System Settings, and it will be black when the user has enabled Dark Theme ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-4.png) 8\. In the **SplashScreen.storyboard Editor**, onto the View Controller canvas, drag-and-drop a new Image ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-9.58.18-AM.png) Add UIImageView 9\. In the **SplashScreen.storyboard Editor**, on the View Controller canvas, drag the Image until the it is aligned to the center of the View Controller ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-10.00.30-AM.png) 10\. In the **SplashScreen.storyboard Editor**, on the View Controller canvas, click on the Image to select it 11\. In the **Properties** window, select **Layout** 12\. In the **Layout** pane, select **Show** \> **Frame Rectangle** 12\. In the **Layout** pane, in the **Origin 3x3 Grid**, click on the center dot > *Note: This ensures that our Image will remain centered, regardless of the screen size of the device* 13\. In the **Layout** pane, select **Layout** \> **Automatic** 14\. In the **Layout** pane, in the **Autosizing** window, ensure only the Up/Down Arrow and the Left/Right Arrow is selected > *Note: This ensures that our Image size will scale up and down, regardless of the screen size of the device* ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-10.02.42-AM.png) ### 2\. Create New Image Asset Next, let's create a new Image Asset that has a Dark Mode version and a Light Mode version using `Assets.xcassets`. 1\. (macOS) In the **Visual Studio Solution Explorer**, in the Xamarin.iOS Project, open **Assets.xcassets** - (PC) In the **Visual Studio Solution Explorer**, in the Xamarin.iOS Project, open **Asset Catalogs** \> **Assets** 2\. In the **Assets.xcassets** window, select **Add** (box with green **+**) > **New Image Set** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-10.21.06-AM.png) Add New Image Set 3\. In the **Assets.xcassets** window, rename the newly created Image Set from Image to **SplashScreenImage** > Note: The name is case-sensitive 4\. In the **Assets.xcassets** window, double-click on the newly created Image Set, **SplashScreenImage** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-1.png) SplashScreenImage Image Set 5\. In the **SplashScreenImage** window, select **Dark theme** 6\. In the **Dark Theme** window, add an image for 2x and 3x > Note: This is the image that will appear on our Splash Screen when the user has enabled Dark Theme. In Dark Theme, my splash screen will be displaying a white image on a black background. > Note: If you are still supporting iOS 6 (or below), be sure to add a 1x image too; 1x images are obsolete if you are only targeting iOS7 and higher. ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-10.53.20-AM.png) Dark Theme SplashScreenImage 7\. In the **SplashScreenImage** window, select **Light theme** 8\. In the **Light Theme** window, add an image for 2x and 3x > Note: This is the image that will appear on our Splash Screen when the user has enabled Light Theme. In Light Theme, my splash screen will be displaying a black image on a white background. > Note: If you are still supporting iOS 6 (or below), be sure to add a 1x image too; 1x images are obsolete if you are only targeting iOS7 and higher. ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-11-at-10.59.22-AM.png) Light Theme SplashScreenImage 9\. In the **Visual Studio Solution Explorer**, in the Xamarin.iOS project, open **SplashScreen.storyboard** 10\. In the **SplashScreen.storyboard Editor**, click on the Image to select it 11\. In the **Properties** window, select **Widget** 12\. In the **Widget** pane, select **Image** \> **SplashScreenImage** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-14.png) Image: SplashScreenImage ## 3\. Set LaunchScreen in Info.plist Now that **SplashScreen.storyboard** is completed, let's configure our Info.plist to ensure our iOS app uses it as the Launch Screen. 1. In the **Visual Studio Solution Explorer**, in the Xamarin.iOS Project, open **Info.plist** 2. In the **Info.plist** editor, set the value of **Launch Screen** to use **SplashScreen** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-2.png) Info.plist Launch Screen ## Android Splash Screen For Android, we'll leverage the **values-night** resource which will automatically detect whether the user has enabled Dark Mode and choose the correct splash screen to display. To do this, we'll create the following items: - Add Splash Screen Image (x2) - Create Drawable XML (x2) - Create Night Theme | Dark Theme Enabled | Dark Theme Disabled | | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | ![Android Splash Screen, Dark Theme Enabled](https://user-images.githubusercontent.com/13558917/66689817-75821700-ec41-11e9-8f62-4ee200f27bc3.png) | ![Android Splash Screen, Dark Theme Disabled](https://user-images.githubusercontent.com/13558917/66689818-761aad80-ec41-11e9-917b-955b4c2e931f.png) | ### 1\. Add Splash Screen Images Let's add two images to our **drawable** folder, one for light theme and one for dark theme. 1\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, right-click **Resources** \> **drawable** 2\. In the right-click menu, select **Add** \> **New File** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-10.png) Add New File, Dark Theme Image 3\. In the **Add New Files** window, select the image to use for our Light Theme Splash Screen > Note: In this example, I am using **MondayPundayBlack.png** to display a black image on a white background 4\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, right-click **Resources** \> **drawable** 5\. In the right-click menu, select **Add** \> **New File** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-11.png) Add New File, Dark Theme Image 6\. In the **Add New Files** window, select the image to use for our Dark Theme Splash Screen > Note: In this example, I am using **MondayPundayWhite.png** to display white image on a black background ## 2\. Create Splash Screen XML In the **drawable** folder, we'll create two XML files for our splash screen; one for light theme and one for dark theme. 1\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, right-click **Resources** \> **drawable** 2\. In the right-click menu, select **Add** \> **New File** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-9.png) Create splash\_screen.xml 3\. In the **New File** window, create a new **XML** called **splash\_screen.xml** > Note: The file name is case-sensitive; ensure it is all lower-case ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-12.png) 4\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, open **Resources** \> **drawable** \> **splash\_screen.xml** 5\. In the **splash\_screen.xml** editor, enter the following code ```xml ``` > Note: In this is example, I am using `android:src="@drawable/MondayPundayBlack` and `android:drawable="@android:color/white` to display a black image on a white background 6\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, right-click **Resources** \> **drawable** 7\. In the right-click menu, select **Add** \> **New File** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-9.png) Create splash\_screen.xml 8\. In the **New File** window, create a new **XML** called **splash\_screen\_night.xml** > Note: The file name is case-sensitive; ensure it is all lower-case ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-13.png) Create splash\_screen\_night.xml 9\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, open **Resources** \> **drawable** \> **splash\_screen\_night.xml** 10\. In the **splash\_screen\_night.xml** editor, enter the following code ```xml ``` > Note: In this is example, I am using `android:src="@drawable/MondayPundayWhite` and `android:drawable="@android:color/black` to display a white image on a black background ### 3\. Add Night Theme Android lets us implement multiple themes which the OS will automatically select for us at runtime based on whether or not the user has enabled Dark Mode in the Android System Settings. In this sample, we'll create a folder called **values-night** to implement our Dark Theme splash screen, and we'll add to an existing folder called **values** to implement our Light Theme splash screen. 1. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, right-click on **Resources** 2. In the right-click menu, select **Add** \> **New Folder** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-5.png) Add New Resources Folder 3\. Name the new folder **values-night** > Note: The folder name is case-sensitive; ensure it is all lower-case 4\. In the **Visual Studio Solution Explorer**, right-click on the newly created folder, **values-night** 5\. In the right-click menu, select **Add New File** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-7.png) Add New File to values-night 6\. In the **New File** window, create a new **Empty XML** **File** named **styles.xml** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image-8.png) 7\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, open the newly created file **Resources > values-night** \> **styles.xml** 8\. In the **styles.xml** editor, enter the following code: ```xml ``` 9\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, open the file **Resources > values** \> **styles.xml** 10\. In the **styles.xml** editor, add the following `style`: ```xml ``` > Note: `MainTheme` is the default theme used in Xamarin.Forms templates, however, your app's theme may have a different name. Ensure `parent=[Your Theme]` uses the main theme of your app. 11\. For reference, here is my complete **styles.xml** file after adding our new `style`: ```xml ``` ### 4\. Implement Splash Screen in MainActivity.cs Now we need to update **MainActivity** to ensure it uses our newly created `LaunchTheme` as the Splash Screen. 1\. In the **Visual Studio Solution Explorer**, in the Xamarin.Android project, open **MainActivity.cs** 2\. In the **MainActivity.cs** editor, update the **Activity** attribute to use the following values: - `Theme = "@style/LaunchTheme"` - `LaunchMode = LaunchMode.SingleTop` Here is an example of my complete **Activity** attribute: ```csharp [Activity(Label = "DarkModeSplashScreen", Icon = "@mipmap/icon", Theme = "@style/LaunchTheme", LaunchMode = LaunchMode.SingleTop, MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] ``` 3\. In the **MainActivity.cs** editor, in the `OnCreate` method, before `base.OnCreate`, add the following line of code: `base.SetTheme(Resource.Style.MainTheme);` Here is an example of the updated `OnCreate` method: ```csharp protected override void OnCreate(Bundle savedInstanceState) { //... base.SetTheme(Resource.Style.MainTheme); base.OnCreate(savedInstanceState); //... } ``` > Note: `MainTheme` is the default theme used in Xamarin.Forms templates, however, your app's theme may have a different name. You can find your app's theme name in **Resources** \> **values** \> **styles.xml** ## Conclusion Now we have a splash screen that will automatically detect the OS's theme and display the appropriate splash screen. The completed solution for this walkthrough can be found here: [https://github.com/brminnick/DarkModeSplashScreen](https://github.com/brminnick/DarkModeSplashScreen?ref=codetraveler.io) To continue on your journey of implementing Dark Mode, I recommend my earlier blog post, [Check for Dark Mode in Xamarin.Forms](https://codetraveler.io/2019/10/11/creating-a-dark-mode-splash-screen/Check%20for%20Dark%20Mode%20in%20Xamarin.Forms). ### Version Conflict Detected for Xamarin.Android.Support URL: https://codetraveler.io/2019/10/08/xamarin-android-nuget-version-conflict-detected/ Last updated: 2019-10-08T21:51:11.000Z Both Xamarin.Forms and Xamarin.Essentials recently increased their `Xamarin.Android.Support` dependency to v28.0.0.3, and it might cause a weird error when updating NuGet Packages. Let's see how to fix it! ## What's The Error? When updating to the latest versions of Xamarin.Forms or Xamarin.Essentials, you may see one of these error messages: `Package restore failed. Rolling back package changes for 'MyApp.Android'.` or `Version conflict detected for Xamarin.Android.Support.Compat` (For nostalgia and SEO purposes, I've pasted the complete error message at the bottom of this blog post) ## What Caused It? Xamarin.Forms v4.2.0.848062 and Xamarin.Essentials v1.3.1 now target v28.0.0.3 of the Xamarin.Android.Support libraries. But, their previous versions targeted v28.0.0.1 of the Xamarin.Android.Support libraries. | Xamarin.Forms v4.2.0.778463 | Xamarin.Forms v4.2.0.848062 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ![Xamarin.Forms v4.2.0.778463 Android Dependencies](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-08-at-2.08.41-PM.png) | ![Xamarin.Forms v4.2.0.848062 Android Dependencies](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-08-at-2.03.12-PM.png) | | Xamarin.Essentials v1.3.0 | Xamarin.Essentials v1.3.1 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ![Xamarin.Essentials v1.3.0 Android Dependencies](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-08-at-2.11.32-PM.png) | ![Xamarin.Essentials v1.3.1 Android Dependencies](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/Screen-Shot-2019-10-08-at-2.13.44-PM.png) | Updating Xamarin.Essentials before updating Xamarin.Forms in our Xamarin.Android project is causing a NuGet Package Dependency conflict where one package needs v28.0.0.3 while the other needs v28.0.0.1\. ## How Do We Fix It? The quickest way to resolve this error is to update the NuGet Package references simultaneously, directly at the source. In the Xamarin.Android project's `csproj` file, we can set the NuGet Version manually: ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/10/image.png) #### Before ```xml ``` #### After ```xml ``` > *Note that I've removed every Xamarin.Android.Support library `PackageReference`* ## Conclusion NuGet packages dependencies can sometimes be a pain, especially when two packages reference different versions of the same NuGet Package. To fix this error in particular, we can update them both simultaneously in the Xamarin.Android `csproj` file. ## Complete NuGet Error ``` GET https://api.nuget.org/v3/registration3-gz-semver2/xamarin.essentials/index.json OK https://api.nuget.org/v3/registration3-gz-semver2/xamarin.essentials/index.json 270ms Getting restore information for project /Users/bramin/Projects/MyApp/MyApp.Android/MyApp.Android.csproj Getting restore information for project /Users/bramin/Projects/MyApp/MyApp.iOS/MyApp.iOS.csproj Running non-parallel restore. Reading project file /Users/bramin/Projects/MyApp/MyApp.Android/MyApp.Android.csproj. Persisting no-op dg to /Users/bramin/Projects/MyApp/MyApp.Android/obj/MyApp.Android.csproj.nuget.dgspec.json Restoring packages for /Users/bramin/Projects/MyApp/MyApp.Android/MyApp.Android.csproj... Restoring packages for MonoAndroid,Version=v9.0... Resolving conflicts for MonoAndroid,Version=v9.0... Version conflict detected for Xamarin.Android.Support.Compat. Install/reference Xamarin.Android.Support.Compat 28.0.0.3 directly to project MyApp.Android to resolve this issue. MyApp.Android -> Xamarin.Essentials 1.3.1 -> Xamarin.Android.Support.Compat (>= 28.0.0.3) MyApp.Android -> Xamarin.Forms 4.2.0.709249 -> Xamarin.Android.Support.v4 28.0.0.1 -> Xamarin.Android.Support.Compat (= 28.0.0.1). Checking compatibility of packages on MonoAndroid,Version=v9.0. Checking compatibility for MyApp.Android 1.0.0 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Essentials 1.3.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Forms 4.2.0.709249 with MonoAndroid,Version=v9.0. Checking compatibility for MyApp 1.0.0 with MonoAndroid,Version=v9.0. Checking compatibility for System.Numerics.Vectors 4.5.0 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Compat 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Core.Utils 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.CustomTabs 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.v4 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Design 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.v7.AppCompat 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.v7.CardView 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Arch.Lifecycle.Runtime 1.1.1.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Collections 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Annotations 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.VersionedParcelable 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.DocumentFile 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Loader 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.LocalBroadcastManager 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Print 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Interpolator 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Core.UI 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Fragment 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Media.Compat 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.v7.RecyclerView 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Transition 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Animated.Vector.Drawable 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.Vector.Drawable 28.0.0.1 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Arch.Core.Common 1.1.1.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Arch.Lifecycle.Common 1.1.1.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Arch.Lifecycle.LiveData 1.1.1.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Arch.Lifecycle.ViewModel 1.1.1.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.AsyncLayoutInflater 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.CoordinaterLayout 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.CursorAdapter 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.CustomView 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.DrawerLayout 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.SlidingPaneLayout 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.SwipeRefreshLayout 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Support.ViewPager 28.0.0.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Arch.Core.Runtime 1.1.1.3 with MonoAndroid,Version=v9.0. Checking compatibility for Xamarin.Android.Arch.Lifecycle.LiveData.Core 1.1.1.3 with MonoAndroid,Version=v9.0. All packages and projects are compatible with MonoAndroid,Version=v9.0. Package restore failed. Rolling back package changes for 'MyApp.Android'. ``` ### Check for Dark Mode in Xamarin.Forms URL: https://codetraveler.io/2019/09/11/check-for-dark-mode-in-xamarin-forms/ Last updated: 2020-04-10T19:38:03.000Z **Update** (10 April 2020)**: C**ustom Renderers are no longer required to implement theming.[Xamarin.Forms has introduced the following APIs](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/Application.cs?ref=codetraveler.io) in Xamarin.Forms Xamarin.Forms v4.6.0.616-pre4: - `Xamarin.Forms.Application.Current.RequestedTheme` - `Xamarin.Forms.Application.Current.RequestedThemeChanged` You can find the completed solution that uses these updated APIs in this app: - [https://github.com/brminnick/GitTrends](https://github.com/brminnick/GitTrends?ref=codetraveler.io) --- Both [iOS 13](https://developer.apple.com/documentation/appkit/supporting%5Fdark%5Fmode%5Fin%5Fyour%5Finterface?ref=codetraveler.io) and [Android Q](https://developer.android.com/guide/topics/ui/look-and-feel/darktheme?ref=codetraveler.io) now let the user enable Dark Mode for the operating system. Let's explore how to check if the user has enabled Dark Mode in our Xamarin.Forms apps! You can find the completed solution in the following apps: - [https://github.com/brminnick/DarkModeSplashScreen](https://github.com/brminnick/DarkModeSplashScreen?ref=codetraveler.io) ## Xamarin.Forms In our Xamarin.Forms project, let's create an interface called `IEnvironment` and an enum called `Theme` that will be used by the platform-specific libraries ```csharp using System.Threading.Tasks; namespace MyNamespace { public interface IEnvironment { Theme GetOperatingSystemTheme(); Task GetOperatingSystemThemeAsync(); } public enum Theme { Light, Dark } } ``` After we've added the platform-specific code (in the Xamarin.iOS and Xamarin.Android sections below), we'll be able to detect the Theme from our Xamarin.Forms code. I recommend checking/setting the theme in `Application.OnStart` and `Application.OnResume` to ensure that our app always adhere's to the user's preference. ```csharp using Xamarin.Forms; namespace MyNamespace { public App : Application { // ... protected override async void OnStart() { base.OnStart(); Theme theme = DependencyService.Get().GetOperatingSystemTheme(); SetTheme(theme); } protected override async void OnResume() { base.OnResume(); Theme theme = DependencyService.Get().GetOperatingSystemTheme(); SetTheme(theme); } void SetTheme(Theme theme) { //Handle Light Theme & Dark Theme } } } ``` ## Xamarin.iOS In our Xamarin.iOS project, we will retrieve the `TraitCollection.UserInterfaceStyle` value from the current `UIViewController` and check whether it is `UIUserInterfaceStyle.Light` or `UIUserInterfaceStyle.Dark`. These APIs were introduced in iOS 12.0, so we'll first check whether the device is running iOS 12 (or higher). If it is on a earlier version of iOS, we will return `Theme.Light`. ```csharp using System; using UIKit; using Xamarin.Forms; using MyNamespace; using MyNamespace.iOS; [assembly: Dependency(typeof(Environment_iOS))] namespace MyNamespace.iOS { public class Environment_iOS : IEnvironment { public Theme GetOperatingSystemTheme() { //Ensure the current device is running 12.0 or higher, because `TraitCollection.UserInterfaceStyle` was introduced in iOS 12.0 if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) { var currentUIViewController = GetVisibleViewController(); var userInterfaceStyle = currentUIViewController.TraitCollection.UserInterfaceStyle; switch (userInterfaceStyle) { case UIUserInterfaceStyle.Light: return Theme.Light; case UIUserInterfaceStyle.Dark: return Theme.Dark; default: throw new NotSupportedException($"UIUserInterfaceStyle {userInterfaceStyle} not supported"); } } else { return Theme.Light; } } // UIApplication.SharedApplication can only be referenced by the Main Thread, so we'll use Device.InvokeOnMainThreadAsync which was introduced in Xamarin.Forms v4.2.0 public async Task GetOperatingSystemThemeAsync() => Device.InvokeOnMainThreadAsync(GetOperatingSystemTheme); static UIViewController GetVisibleViewController() { UIViewController viewController = null; var window = UIApplication.SharedApplication.KeyWindow; if (window.WindowLevel == UIWindowLevel.Normal) viewController = window.RootViewController; if (viewController is null) { window = UIApplication.SharedApplication .Windows .OrderByDescending(w => w.WindowLevel) .FirstOrDefault(w => w.RootViewController != null && w.WindowLevel == UIWindowLevel.Normal); viewController = window?.RootViewController ?? throw new InvalidOperationException("Could not find current view controller."); } while (viewController.PresentedViewController != null) viewController = viewController.PresentedViewController; return viewController; } } } ``` ## Xamarin.Android In our Xamarin.Android project, we'll retrieve `Configuration.UiMode` from the current `Context` and check whether it is `UiMode.NightYes` or `UiMode.NightNo`. **Note**: This code requires the [Plugin.CurrentActivity NuGet Package](https://www.nuget.org/packages/Plugin.CurrentActivity/?ref=codetraveler.io). Make sure to first add this NuGet package to your Xamarin.Android project. ```csharp using System; using System.Threading.Tasks; using Android.Content.Res; using Plugin.CurrentActivity; using Xamarin.Forms; using MyNamespace; using MyNamespace.Android; [assembly: Dependency(typeof(Environment_Android))] namespace MyNamespace.Android { public class Environment_Android : IEnvironment { public Task GetOperatingSystemThemeAsync() => Task.FromResult(GetOperatingSystemTheme()); public Theme GetOperatingSystemTheme() { //Ensure the device is running Android Froyo or higher because UIMode was added in Android Froyo, API 8.0 if(Build.VERSION.SdkInt >= BuildVersionCodes.Froyo) { var uiModeFlags = CrossCurrentActivity.Current.AppContext.Resources.Configuration.UiMode & UiMode.NightMask; switch(uiModelFlags) { case UiMode.NightYes: return Theme.Dark; case UiMode.NightNo: return Theme.Light; default: throw new NotSupportedException($"UiMode {uiModelFlags} not supported"); } } else { return Theme.Light; } } } } ``` ### Making ObservableCollection Thread-Safe in Xamarin.Forms URL: https://codetraveler.io/2019/09/11/using-observablecollection-in-a-multi-threaded-xamarin-forms-application/ Last updated: 2019-09-11T18:54:20.000Z `ObservableCollection` is the [recommended collection](https://docs.microsoft.com/xamarin/xamarin-forms/user-interface/listview/data-and-databinding?WT.mc%5Fid=threadsafeobservablecollection-codetraveler-bramin&ref=codetraveler.io) to use for ListViews, but it isn't thread safe. Let's explore how we can fix this and use it in our multi-threaded apps! The Xamarin.Forms team [recommends using ObservableCollection](https://docs.microsoft.com/xamarin/xamarin-forms/user-interface/listview/data-and-databinding?WT.mc%5Fid=threadsafeobservablecollection-codetraveler-bramin&ref=codetraveler.io) for `ListView.ItemSource`, but when we then try to update the collection from different threads, we'll get this error because `ObservableCollection` isn't thread safe: ``` System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. Parameter name: index ListProxy.get_Item (System.Int32 index) D:\a\1\s\Xamarin.Forms.Core\ListProxy.cs:129 IList.get_Item (System.Int32 index) ``` Luckily, the fix is pretty easy! ## Solution There is a library included in Xamarin.Forms that we can use to ensure the `ObservableCollection` is only updated by one thread at a time: `` `[BindingBase.EnableCollectionSynchronization](https://docs.microsoft.com/dotnet/api/xamarin.forms.bindingbase.enablecollectionsynchronization?view=xamarin-forms&WT.mc%5Fid=threadsafeobservablecollection-codetraveler-bramin&ref=codetraveler.io)` ``. We just need to call this method in our constructor after initializing our `ObservableCollection` and our collection becomes thread safe: ```csharp class MyViewModel { public MyViewModel() { MyCollection = new ObservableCollection(); Xamarin.Forms.BindingBase.EnableCollectionSynchronization(MyCollection, null, ObservableCollectionCallback); } public ObservableCollection MyCollection { get; } void ObservableCollectionCallback(IEnumerable collection, object context, Action accessMethod, bool writeAccess) { // `lock` ensures that only one thread access the collection at a time lock (collection) { accessMethod?.Invoke(); } } } ``` ### Using Preview Versions of .NET Core with App Center Build URL: https://codetraveler.io/2019/08/23/using-preview-versions-of-net-core-with-app-center-build/ Last updated: 2019-08-28T17:30:36.000Z [App Center](https://appcenter.ms/?WT.mc%5Fid=netcoreappcenterbuild-codetraveler-bramin&ref=codetraveler.io) is a great tool for building, testing, releasing and monitoring apps, but what if we want to use a preview version of .NET Core? Today, App Center Build doesn't support the preview versions .NET Core. This means that when I use .NET Core 3.0, my build fails with this error: ``` The current .NET SDK does not support targeting .NET Core 3.0 ``` (Here's the complete error message) ``` ##[section]Starting: Restore Nuget ============================================================================== Task : Command Line Description : Run a command line with arguments Version : 1.1.3 Author : Microsoft Corporation Help : [More Information](https://go.microsoft.com/fwlink/?LinkID=613735) ============================================================================== [command]/bin/bash -c /usr/bin/find . -name '*.sln' -type f -print0 | /usr/bin/xargs -0 grep -l 'MondayPundayApp.Droid.csproj' | /usr/bin/xargs -I '{}' nuget restore '{}' -DisableParallelProcessing MSBuild auto-detection: using msbuild version '15.0' from '/Library/Frameworks/Mono.framework/Versions/6.0.0/lib/mono/msbuild/15.0/bin'. /Users/vsts/hostedtoolcache/dotnet/sdk/2.2.300/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(137,5): error NETSDK1045: The current .NET SDK does not support targeting .NET Core 3.0. Either target .NET Core 2.2 or lower, or use a version of the .NET SDK that supports .NET Core 3.0. [/Users/vsts/agent/2.155.1/work/1/s/Src/MondayPundayApp.UnitTests/MondayPundayApp.UnitTests.csproj] ##[error]/bin/bash failed with return code: 1 ##[error]/bin/bash failed with error: /bin/bash failed with return code: 1 ``` To work-around this problem, let's write a script that installs the version of .NET Core we need. ## 1\. Write a Post-Clone Script App Center Build allows us to write a [bash script that runs after the build machine has finished cloning our repo](https://docs.microsoft.com/appcenter/build/custom/scripts?WT.mc%5Fid=netcoreappcenterbuild-codetraveler-bramin&ref=codetraveler.io#post-clone), called a `Post-Clone` script: 1\. In the same directory as the Android `.csproj`, create a new file called `appcenter-post-clone.sh` > Place the scripts with the format specified below next to the project-level (.xcodeproj, .csproj, .sln or package.json) file ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-23-at-10.55.00-AM.png) 2\. (Repeat for iOS) In the same directory as the iOS `.csproj`, create a new file called `appcenter-post-clone.sh` 3\. Open both `appcenter-post-clone.sh` files 4\. In `appcenter-post-clone.sh`, copy the following Bash script: ```bash wget https://dot.net/v1/dotnet-install.sh chmod +x dotnet-install.sh ./dotnet-install.sh --version $netcoreversion --install-dir "$AGENT_TOOLSDIRECTORY/dotnet" ``` 5\. Commit the new `appcenter-post-clone.sh` files and push the updates to your remote git repository ## 2\. Update App Center Build Settings 1\. In [App Center](https://appcenter.ms/?WT.mc%5Fid=netcoreappcenterbuild-codetraveler-bramin&ref=codetraveler.io), navigate to your app's **Build** configuration ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-23-at-10.10.48-AM.png) 2\. On the **Build Branches** page, select the **Settings Gear** next to the branch you'd like to configure ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-23-at-9.43.54-AM.png) 3\. In the **Build Configuration** menu, select **More Options** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-23-at-9.44.05-AM.png) 4\. In the **More Options** section of the **Build Configuration** menu, ensure `Post-Clone` is listed as a build script **Note**: If `Post-Clone` is not listed, ensure that you've saved `appcenter-post-clone.sh` its [project-level directory](https://docs.microsoft.com/appcenter/build/custom/scripts/index?WT.mc%5Fid=netcoreappcenterbuild-codetraveler-bramin&ref=codetraveler.io) and have commited the file to your remote git repository ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-23-at-9.44.14-AM.png) 5\. In the **Build Configuration** menu, in the **Environment Variables** section, add the following key-value par: **Note**: You can specify any version of .NET Core as the **Value** ``` | Name | Value | | netcoreversion | 3.0.100-preview8-013656 | ``` ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-23-at-9.45.11-AM.png) 6\. In the **Build Configuration** menu, select **Save & Build** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/08/Screen-Shot-2019-08-23-at-9.45.20-AM.png) ### Schedule Recurring Builds in App Center URL: https://codetraveler.io/2019/06/06/scheduling-app-center-builds/ Last updated: 2019-06-06T21:21:00.000Z Let's explore how to schedule recurring builds in [App Center](https://azure.microsoft.com/services/app-center?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io) using [Azure Timer Functions](https://docs.microsoft.com/azure/azure-functions/functions-bindings-timer?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io) Today, [App Center Build](https://docs.microsoft.com/appcenter/build?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io) offers a continuous integration solution for iOS, Android and UWP apps that will trigger each time we commit code to our repository. However, it [doesn't yet allow us to schedule recurring builds](https://github.com/microsoft/appcenter/issues/328?ref=codetraveler.io). Luckily, App Center has a [complete suite of APIs](https://docs.microsoft.com/appcenter/api-docs?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io) we can leverage to schedule an [Azure Timer Function](https://docs.microsoft.com/azure/azure-functions/functions-bindings-timer?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io) (essentially a [cron job](https://en.wikipedia.org/wiki/Cron?ref=codetraveler.io) in the cloud) to trigger a build every night. This walkthrough shows how to gather the required metadata and API Token from App Center, then how to build an Azure Timer Function that will trigger a build via the App Center APIs. For a complete solution, see the UITestSampleApp.Functions project in this repo: [https://github.com/brminnick/UITestSampleApp](https://github.com/brminnick/UITestSampleApp?ref=codetraveler.io) ## 1\. Get App Center Metadata First, let's grab our App Center app's name, owner and branch metadata 1. In the [**App Center Portal**](https://appcenter.ms/?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io), click into the app - **Note:** My app is called **UITestSampleApp, Android** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture5.png) Click Into App 2\. In the **App Overview** page, click on the **Build** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture6.png) Select Build Icon 3\. On the **Branches** page, click into the branch for which you'd like to schedule a recurring build - **Note:** I am using the **master** branch ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture7.png) Click Into Branch 4\. On the **Builds** page, using the URL, copy the values for the **Owner**, **App Name** and **Branch** - **Note:** The URL will follow the following pattern: *https://appcenter.ms/users/{owner\_name}/apps/ {app\_name}/build/branches/{branch\_name}* ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/AppCenterMetaData-1.png) Collect Metadata ## 2\. Generate App Center API Token Next, let's generate a token in App Center that we'll use to access our API. 1. In the [App Center Portal](https://appcenter.ms/?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io), on the top-right, click on your profile photo 2. In the settings drop-down, select **Account Settings** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture1.png) 3\. On the **Account Settings** page, click **API Tokens** 4\. On the **API Tokens** page, select **New API Token** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture2.png) New API Token 5\. On the **New API Token** page, enter a **Description** - **Note:** For my description, I used **Azure Timer Function API Token** 6\. On the **New API Token** page, select **Full Access** 7\. On the **New API Token** page, select **Add new API token** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture3.png) Add new API token 8\. On the **API Tokens** page, click the copy icon to copy the API Token to the clipboard - **Note:** We will use this API token later for our Azure Function ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture4.png) Copy API Token ## 3\. Create Azure Timer Function Now, let's create an [Azure Timer Function](https://docs.microsoft.com/azure/azure-functions/functions-bindings-timer?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io) to schedule a recurring build 1. In the [Azure Portal](https://portal.azure.com/?WT.mc%5Fid=appcenterbuilds-codetraveler-bramin&ref=codetraveler.io), click the **Create a Resource** button ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Picture9.png) New Azure Resource 2\. In the search bar, enter **Function App** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/FunctionAppSearch.png) Search Function App 3\. In the search results, select **Function App** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/SearchResults.png) Function App Search Results 4\. On the **Marketplace Dashboard**, select **Create** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/CreateFunctin.png) Create Function 5\. On the **Create Function App** page, enter the following details: - **App name:** \[Your function app name\] - **Note:** The app name must be unique as it is used in the URL - **Subscription:** \[Select your Azure Subscription\] - **Resource Group** - Create New - AppCenterFunction - **OS:** Windows - **Hosting Plan:** Consumption - **Location:** \[Select the location closest to you\] - **Runtime Stack:** .NET - **Storage** - Create New - \[Your storage account name\] - **Application Insights:** \[Use defaults\] 6\. On the **Create Function App** page, click **Create** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/CreateFunction-1.png) Configure Function App 7\. Stand by while the Function App is deployed ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/DeploymentProgress.png) 8\. Once the deployment has finished, on the Azure Portal, select the notification button (bell icon) 9\. In the **Notifications** menu, select **Go to resource** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/GoToResource.png) Go To Resource 10\. On the **Function App Dashboard**, next to **Functions**, click the Plus Sign (**+**) 11\. On the **Quickstart** dashboard, select **In-Portal** 12\. On the **Quickstart** dashboard, click **Continue** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/InPortalFunction-2.png) Create In-Portal Function 13\. In the **Quickstart** dashboard, select **Timer** 14\. In the **Quickstart** dashboard, select **Continue** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/InPortalTimerTrigger.png) Create Timer Function 15\. In the **run.csx** editor, enter the following code - **Note:** Change `owner`, `appName`, and `branch` to match the values retrieved in the previous step, *Get App Center Metadata* ```csharp using System; public static async Task Run(TimerInfo myTimer, ILogger log) { const string owner = "bminnick"; //change to your app owner const string appName = "uitestsampleapp"; // change to your app name const string branch = "master"; //change to your repo's branch var client = new HttpClient(); client.DefaultRequestHeaders.Add("X-API-Token", Environment.GetEnvironmentVariable("AppCenterApiToken")); var httpContent = new StringContent("{ \"debug\": true }", System.Text.Encoding.UTF8, "application/json"); var result = await client.PostAsync($"https://api.appcenter.ms/v0.1/apps/{owner}/{appName}/branches/{branch}/builds", httpContent); result.EnsureSuccessStatusCode(); } ``` 16\. In the **run.csx** editor, click **Save** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/SaveCSX-1.png) Save run.csx 17\. In the **Functions Dashboard**,on the left-hand menu, select **Integrate** 18\. In the **Integrate** dashboard, in the **Timestamp parameter name** text box, enter **`myTimer`** 19\. In the **Integrate** dashboard, in the **Schedule** text box, enter your cron schedule - **Note:** I am using `0 0 9 * * *`, which means my Timer Function will run at 0900 UTC every day 20\. In the **Integrate** dashboard, click **Save** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/Integrate-1.png) 21\. On the Functions dashboard, on the left-hand menu, select the name of your **Function App** - **Note:** My Function App is named **AppCenterFunction** 22\. On the Functions **Overview** Dashboard, select **Configuration** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/FunctionsConfiguration-1.png) Function app settings 23\. In **Application Settings**, click **New application setting** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/NewApplicationSetting.png) New Application Setting 24\. In the **Add/Edit application setting** window, in the **Name** text box, enter `AppCenterApiToken` 25\. In the **Add/Edit application setting** window, in the **Value** text box, enter the App Center API Token generated in the earlier step, *Generate App Center API Token* 26\. In the **Add/Edit application setting** window, click **Update** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/ApiToken.png) Add AppCenterApiToken 27\. In **Application Settings**, click **Save** ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/06/SaveApplicationSettings.png) That's it! We now have created an Azure Timer Function that will trigger an App Center Build. ### Keeping up with Xamarin URL: https://codetraveler.io/2019/05/09/keeping-up-with-xamarin/ Last updated: 2019-05-10T18:06:53.000Z Here are the tricks I use to stay up-to-date with Xamarin, iOS, Android and .NET. ### Blog Posts [Planet Xamarin](https://www.planetxamarin.com/?ref=codetraveler.io) aggregates content from the Xamarin Community into one news feed. This is where I discover all of the great blog posts from fellow Xamarin developers as well as the official blog posts from the Xamarin team at Microsoft. I use [Feedly](https://feedly.com/?ref=codetraveler.io) to keep up with its RSS Feed. ### Newsletters These newsletters all deliver high quality content once per week - [C# Digest](https://csharpdigest.net/digests?ref=codetraveler.io) \- Trending C# Blogs - [Gitly](http://www.gitly.co/?ref=codetraveler.io) \- Trending GitHub Repos - [Programming Digest](https://programmingdigest.net/?ref=codetraveler.io) \- Trending general programming blogs - [Weekly Xamarin](http://weeklyxamarin.com/?ref=codetraveler.io) \- Trending Xamarin topics ### GitHub Repositories GitHub [recently released a new feature](https://github.blog/changelog/2018-11-27-watch-releases/?ref=codetraveler.io) that allows us to be notified any time a new release is published by an open-source repo. You can enable these notifications by selecting the "Releases only" option for each repo. With this enabled, GitHub sends me an email notification each time an update has been released. Here's a few repos to watch: - [.NET CoreFX](https://github.com/dotnet/corefx?ref=codetraveler.io) - [.NET CoreCLR](https://github.com/dotnet/coreclr?ref=codetraveler.io) - [.NET Standard](https://github.com/dotnet/standard?ref=codetraveler.io) - [Mono](https://github.com/mono/mono?ref=codetraveler.io) - [Xamarin.Android](https://github.com/xamarin/xamarin-android?ref=codetraveler.io) - [Xamarin.Android.Support Libraries](https://github.com/xamarin/AndroidSupportComponents?ref=codetraveler.io) - [Xamarin Components](https://github.com/xamarin/XamarinComponents?ref=codetraveler.io) - [Xamarin.Essentials](https://github.com/xamarin/Essentials?ref=codetraveler.io) - [Xamarin.Forms](https://github.com/xamarin/Xamarin.Forms?ref=codetraveler.io) - [Xamarin Google Play Services Components](https://github.com/xamarin/GooglePlayServicesComponents?ref=codetraveler.io) - [Xamarin.iOS](https://github.com/xamarin/xamarin-macios?ref=codetraveler.io) ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/05/image.png) Watch GitHub Releases ### GitHub Users Along with watching repos, I also follow a few influential .NET / Xamarin developers. And I use [DevHub](https://devhubapp.com/?ref=codetraveler.io) to stay up-to-date with their latest activities on GitHub; it's basically like TweetDeck for GitHub. Here are a few of the folks I follow on GitHub: - [Miguel de Icaza](https://github.com/migueldeicaza?ref=codetraveler.io) \- Founder, Mono & Xamarin - [James Montemagno](https://github.com/jamesmontemagno?ref=codetraveler.io) \- Principal Program Manager, Xamarin - [Mads Torgensen](https://github.com/MadsTorgersen?ref=codetraveler.io) \- Principal Program Manager, C# - [Frank Krueger](https://github.com/praeclarum?ref=codetraveler.io) \- Mobile App Developer, Creator of [sqlite-net](https://github.com/praeclarum/sqlite-net?ref=codetraveler.io) - [Scott Hanselman](https://github.com/shanselman?ref=codetraveler.io) \- .NET Extrordinaire - [Jim Bennett](https://github.com/jimbobbennett?ref=codetraveler.io) \- Developer Advocate, Xamarin - [Matt Soucoup](https://github.com/codemillmatt?ref=codetraveler.io) \- Developer Advocate, Xamarin - [Lauren Bugnion](https://github.com/lbugnion?ref=codetraveler.io) \- Developer Advocate, Xamarin - (me) [Brandon Minnick](https://github.com/brminnick?ref=codetraveler.io) \- Developer Advocate, Xamarin ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/05/Screen-Shot-2019-05-10-at-11.05.15-AM.png) Follow GitHub Users ### Podcasts There are also many great .NET, Xamarin and mobile podcasts. I use [Overcast](https://overcast.fm/podcasts?ref=codetraveler.io) to subscribe and listen to podcasts. Here are a few of my favorite podcasts: - [Android Developers Backstage](https://overcast.fm/itunes785545036/android-developers-backstage?ref=codetraveler.io) \- hosted by the Android development team - [Coding Blocks](https://overcast.fm/itunes769189585/coding-blocks?ref=codetraveler.io) \- .NET programming - [Complete Developer Podcast](https://overcast.fm/itunes1039210992/complete-developer-podcast?ref=codetraveler.io) \- For coders by coders about all aspects of creating your best life as a developer - [If Then](https://overcast.fm/itunes1302281912/if-then-news-on-technology-silicon-valley-politics-and-tech-policy?ref=codetraveler.io) \- Weekly tech news - [Merge Conflict](https://overcast.fm/itunes1133064277/merge-conflict?ref=codetraveler.io) \- C#, F#, .NET, Web & mobile - [Techmeme Ride Home](https://overcast.fm/itunes1355212895/techmeme-ride-home?ref=codetraveler.io) \- Daily tech news - [The Xamarin Podcast](https://overcast.fm/itunes691368176/the-xamarin-podcast?ref=codetraveler.io) \- The official Xamarin podcast ### iOS / Android News To stay up-to-date with the latest iOS and Android releases, I follow the top stories on [MacRumors](https://www.macrumors.com/?ref=codetraveler.io) and [AndroidCentral](https://www.androidcentral.com/?ref=codetraveler.io) ### [Reddit](https://www.reddit.com/?ref=codetraveler.io) If you are a fellow redditor, here's a few of the subreddits I recommend: - [/r/Android](https://www.reddit.com/r/Android/?ref=codetraveler.io) - [/r/AndroidDev](https://www.reddit.com/r/androiddev/?ref=codetraveler.io) - [/r/Apple](https://www.reddit.com/r/apple/?ref=codetraveler.io) - [/r/CoolGitHubProjects](https://www.reddit.com/r/coolgithubprojects/?ref=codetraveler.io) - [/r/CSharp](https://www.reddit.com/r/csharp/?ref=codetraveler.io) - [/r/DotNet](https://www.reddit.com/r/dotnet/?ref=codetraveler.io) - [/r/iOS](https://www.reddit.com/r/ios/?ref=codetraveler.io) - [/r/Microsoft](https://www.reddit.com/r/microsoft/?ref=codetraveler.io) - [/r/ProgrammerHumor](https://www.reddit.com/r/ProgrammerHumor/?ref=codetraveler.io) - [/r/Programming](https://www.reddit.com/r/programming/?ref=codetraveler.io) - [/r/VisualStudio](https://www.reddit.com/r/VisualStudio/?ref=codetraveler.io) - [/r/XamarinDevelopers](https://www.reddit.com/r/xamarindevelopers/?ref=codetraveler.io) ### ### Using Postman with GraphQL APIs URL: https://codetraveler.io/2019/01/12/how-to-use-postman-with-a-graphql-api/ Last updated: 2019-05-06T15:41:51.000Z Let's explore two ways to use [Postman](https://www.getpostman.com/?ref=codetraveler.io) to interact with GraphQL APIs If you're like me, you've been using [Postman](https://www.getpostman.com/?ref=codetraveler.io) to inspect REST APIs for years. But with the advent of GraphQL, how can we continue using this awesome tool? First, let's find a valid query for an existing GraphQL endpoint. # Exploring GraphQL APIs using GraphiQL Let's find a query for the [Star Wars API](https://swapi.apis.guru/?ref=codetraveler.io), aka SWAPI, using GraphiQL: [https://swapi.apis.guru/graphiql](https://swapi.apis.guru/graphiql?ref=codetraveler.io). This query returns the `id`, `title` and `episodeID` for the six original Star Wars films. ``` query{ allFilms { films { id title episodeID } } } ``` Here's how it looks in GraphiQL. ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/01/image.png) Great! Now how do we do this in Postman? # Exploring GraphQL APIs using Postman Each GraphQL Endpoint is a POST API, and its Body is just one `JSON` object: `query`. We can take the same query that we used above in GraphiQL, to create the `JSON` request. ```json { "query" : "query { allFilms { films { id title episodeID } } }" } ``` ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/01/image-1.png) # Bonus: application/graphql If the GraphQL endpoint accepts the header `application/graphql`, we don't need to use JSON in the Body. First, add the header `Content-type: application/graphql`. ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/01/image-2.png) Now we can use the same GraphQL query that we used above in GraphiQL for the request body. ![](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2019/01/image-3.png) ### Traveler: Los Angeles URL: https://codetraveler.io/2018/06/26/untitled/ Last updated: 2018-08-20T19:18:03.000Z ## Where am I? I am currently at the [Andaz West Hollywood Hotel](https://westhollywood.andaz.hyatt.com/en/hotel/home.html?ref=codetraveler.io) just outside of Los Angeles, California. I'm in town volunteering at [Athena Hacks 2018](http://athenahacks.com/?ref=codetraveler.io). Athena Hacks is an incredible hackathon hosted by the University of Southern California! It is an all-female hackathon whose mission is to support and nurture women in tech (trans and non-binary inclusive). ## How's the Internet Speed? ## ⭐️️️️️️️ I'm incredibly disapointed in the internet speed: it is throttled to 5Mbps. It also seems that my throttled connection is shared between my iPhone and my Mac. Call me crazy, but when I start streaming audio/video on my Mac, the internet speed on my iPhone grinds to a halt. Very strange. ## How's the Internet Reliability? ## ⭐️ Also disapointing. There are deadspots in the hotel room where the WiFi is noticiably slower, and the internet connection has dropped more than a few times. ## How's the Comfort? ## ⭐️⭐️⭐️⭐️⭐️ This hotel room is insanely nice! The front desk upgraded me to a corner suite which is about 3 times bigger than my apartment at home! (Mind you, I live in a 500 sq.ft. studio apartment in SF). There's a sunroom with lounge chairs and floor-to-ceiling windows that overlooks the city of LA; a separate living room with couches, a TV and a 6-person dinner table; a bedroom the size of my apartment with a cozy king-sized bed; and a bathroom with two sinks, a waterfall shower, and a separate jacuzzi bathtub. As I type this, I am lounging in the sunroom, enjoying the view, wearing a fancy hotel robe, and enjoying free snacks and soda from the mini fridge. I could get used to this life! ## How's the Noise? ## ⭐️⭐️⭐️ The noise from traffic outside is noticable. It's not super loud, but it can be distracting. ## General Ambiance ️ ## ⭐️⭐️⭐️⭐️⭐️ The hotel has very chic decor with a retro-hollywood vibe. I'm a big fan. ## Overall Score: ️ ## ⭐️⭐️⭐️ This hotel room is incredibly luxurious, however, the WiFi leaves a lot to be desired. While I would 10/10 stay in this hotel again, it's not a great spot to get work done. So if you are looking for a place to unplug and relax, check out the Andaz West Hollywood! You won't regret it! ### Walkthough: Creating UI Tests for Xamarin.Forms Apps, Part 2 URL: https://codetraveler.io/2018/06/26/creating-ui-tests-for-xamarin-forms-apps-part-2/ Last updated: 2020-06-02T16:23:23.000Z In [Part 1](https://www.codetraveler.io/2018/05/22/creating-uitests-for-xamarin-forms-apps-part-1/?ref=codetraveler.io#traveler), we created a File -> New Xamarin.Forms app, and added UI tests to it. In this post we'll learn how to create scalable UI tests that allow us to reuse code using Page Object architecture. We'll take an existing app, and add a Xamarin.UITest project using the Page Object architecture. ## Why Page Object Architecture? I know what you're thinking: "I already have UI tests written. Why would I want to go back and rewrite all of that code?". The goal of the Page Object architecture is to reuse as much code as possible Check out how much duplicate code exists between these two UI tests: ```csharp [Test] public void UpdateFirstName() { app.Screenshot("App Started"); app.WaitForElement(x => x.Marked("Armstead, Evan")); app.Screenshot("List Displayed"); app.Tap(x => x.Marked("Armstead, Evan")); app.Screenshot("Evan Details Displayed"); app.Tap(x => x.Marked("Edit")); app.Screenshot("Edit Screen Displayed"); app.ScrollDownTo("First"); app.Tap(x => x.Marked("Evan")); app.ClearText(); app.Screenshot("Cleared first name field"); app.EnterText("Jonathan"); app.DismissKeyboard(); app.Screenshot("Entered Jonathan"); app.Tap(x => x.Marked("Save")); app.Screenshot("First Name Changed"); } ``` ```csharp [Test] public void UpdateLastName() { app.Screenshot("App Started"); app.WaitForElement(x => x.Marked("Armstead, Evan")); app.Screenshot("List Displayed"); app.Tap(x => x.Marked("Armstead, Evan")); app.Screenshot("Evan Details Displayed"); app.Tap(x => x.Marked("Edit")); app.Screenshot("Edit Screen Displayed"); app.ScrollDownTo("Last"); app.Tap(x => x.Marked("Armstead")); app.ClearText(); app.Screenshot("Cleared Last Name"); app.EnterText("Smith"); app.DismissKeyboard(); app.Screenshot("Entered Smith"); app.Tap(x => x.Marked("Save")); app.Screenshot("Last Name Changed"); } ``` There's at least 8 lines of code that are exactly the same bewteen these two tests! As we write more and more tests like these, the amount of duplicated code continues to grow. Then, if we decide to refresh the UI, we have to change the same duplicated line of code multiple times, and it becomes a maintenance nightmere! Enter: Page Object Architecture. ## What is Page Object Architecture? In the Page Object Architecture, we will create a class for each page in our app. Each page class will contain all of the actions that a user can perform on the page, e.g. tapping a button, entering text, etc. Using Page Object Architecture, our tests now reuse code and become more easily readable! Take a look at how the same tests look using Page Object Architecture: ```csharp [Test] public void UpdateFirstName() { ContactsListPage.WaitForListToLoad(); ContactsListPage.TapContact("Armstead, Evan"); ContactDetailsPage.TapEdit(); ContactDetailsPage.ClearFirstName(); ContactDetailsPage.EnterFirstName("Jonathan"); ContactDetailsPage.TapSaveButton(); } ``` ```csharp [Test] public void UpdateLastName() { ContactsListPage.WaitForListToLoad(); ContactsListPage.TapContact("Armstead, Evan"); ContactDetailsPage.TapEdit(); ContactDetailsPage.ClearLastName(); ContactDetailsPage.EnterLastName("Smith"); ContactDetailsPage.TapSaveButton(); } ``` ## Implementing Page Object Architecture **Quick Note**: Before writing any UI tests, make sure you've assigned an AutomationId to each UI control in your app. I recommend creating a unique `public const string` for each AutomationId, and putting all of the string constants into a Shared Project that both your Xamarin.Forms project and UI test project can reference. This makes it super easy to reference each AutomationId in our UI test. [Here's an example of how I've implemented it for today's project](https://github.com/brminnick/CosmosDbSampleApp/blob/master/CosmosDbSampleApp.Shared/Constants/AutomationIdConstants.cs?ref=codetraveler.io). ### 1\. Add a UI Test Project Right-click on the Solution and select Add -> New Project ![Screen-Shot-2018-02-25-at-6.03.05-PM](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/Screen-Shot-2018-02-25-at-6.03.05-PM.png) Select Multiplatform -> Tests -> UI Test App ![Screen-Shot-2018-02-25-at-6.04.46-PM](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/Screen-Shot-2018-02-25-at-6.04.46-PM.png) Name the UITest Project \[Solution\].UITests ![Screen-Shot-2018-02-25-at-6.05.19-PM](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/Screen-Shot-2018-02-25-at-6.05.19-PM.png) ### 2\. Update and Install NuGet Packages Update Xamarin.UITest NuGet Package ![Screen-Shot-2018-02-25-at-6.06.15-PM-1](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/Screen-Shot-2018-02-25-at-6.06.15-PM-1.png) Install Xamarin.TestCloud.Agent NuGet Package into the iOS Project (Why? The UI Test Automation APIs on iOS are private, the Xamarin.TestCloud.Agent NuGet Package will expose the APIs. Android's automation APIs are already public, so nothing extra needs to be added to the Android project.) ![Screen-Shot-2018-02-25-at-6.07.16-PM](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/Screen-Shot-2018-02-25-at-6.07.16-PM.png) ![Screen-Shot-2018-02-25-at-6.07.44-PM](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/Screen-Shot-2018-02-25-at-6.07.44-PM.png) Expose iOS's Automation APIs by calling `Xamarin.Calabash.Start()` in `AppDelegate.FinishedLaunching`. Wrap it in the compiler attribute `#if DEBUG` to ensure that your submission to the iOS App Store does not have iOS Automation APIs enabled becuase otherwise [Apple will reject your app](https://docs.microsoft.com/appcenter/test-cloud/uitest/get-started-xamarin-forms?WT.mc%5Fid=xamarinuitests-codetraveler-bramin&ref=codetraveler.io#ios-application-project): > The Calabash assembly makes uses of non-public Apple API's which will cause apps to be rejected by the App Store ```csharp [Register(nameof(AppDelegate))] public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate { public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions) { global::Xamarin.Forms.Forms.Init(); EntryCustomReturn.Forms.Plugin.iOS.CustomReturnEntryRenderer.Init(); #if DEBUG Xamarin.Calabash.Start(); #endif LoadApplication(new App()); return base.FinishedLaunching(uiApplication, launchOptions); } } ``` ### 3\. Add BasePage.cs Add a new `.cs` file to the UITests project called `BasePage.cs` ```csharp public abstract class BasePage { protected BasePage(IApp app, string pageTitle) { App = app; Title = pageTitle; } public string Title { get; } protected IApp App { get; } public virtual void WaitForPageToLoad() => App.WaitForElement(Title); } ``` ### 4\. Create Page Class Add a new `.cs` file to the UITests project called `[YourPageName]Page.cs`. In this example, I am creating `PersonListPage.cs`. First, add this custom `using` statement so that we don't have to repeatedly type `System.Func` ```csharp using Query = System.Func; ``` Next, make sure your page class inhereits from `BasePage`. Here's what my `PersonListPage` class looks like so far: ```csharp using Query = System.Func; namespace CosmosDbSampleApp.UITests { public class PersonListPage : BasePage { } } ``` In the page class, let's add a `Query` field for each UI control on the page. Here's what my `PersonListPage` class looks like so far: ```csharp using Query = System.Func; namespace CosmosDbSampleApp.UITests { public class PersonListPage : BasePage { readonly Query _personList, _activityIndicator, _addButton; } } ``` Now let's initialize the `Query` fields in the constructor using `x => x.Marked("[Control's Automation Id]");`. Here's what my `PersonListPage` looks like so far: ```csharp using Query = System.Func; namespace CosmosDbSampleApp.UITests { public class PersonListPage : BasePage { readonly Query _personList, _activityIndicator, _addButton; public PersonListPage(IApp app, string pageTitle) : base(app, pageTitle) { _personList = x => x.Marked(AutomationIdConstants.PersonListPage_PersonList); _activityIndicator = x => x.Marked(AutomationIdConstants.PersonListPage_ActivityIndicator); _addButton = x => x.Marked(AutomationIdConstants.PersonListPage_AddButton); } } } ``` Lastly, let's add a public method for each action a user can do on the page. For example, if a user can tap a button on the page, let's create a method for that. Here's the final code for my PersonListPage class: ```csharp using Xamarin.UITest; using CosmosDbSampleApp.Shared; using Query = System.Func; namespace CosmosDbSampleApp.UITests { public class PersonListPage : BasePage { readonly Query _personList, _activityIndicator, _addButton; public PersonListPage(IApp app, string pageTitle) : base(app, pageTitle) { _personList = x => x.Marked(AutomationIdConstants.PersonListPage_PersonList); _activityIndicator = x => x.Marked(AutomationIdConstants.PersonListPage_ActivityIndicator); _addButton = x => x.Marked(AutomationIdConstants.PersonListPage_AddButton); } public void TapAddButton() { App.Tap(_addButton); App.Screenshot("Add Button Tapped"); } public void WaitForActivityIndicator() { App.WaitForElement(_activityIndicator); App.Screenshot("Activity Indicator Appeared"); } public void WaitForNoActivityIndicator() { App.WaitForNoElement(_activityIndicator); App.Screenshot("Activity Indicator Disappeared"); } } } ``` Repeat this step and create a page class for each page in the app. ### 5\. Add BaseTest.cs Add a new `.cs` file to the UITests project called `BaseTest.cs` ```csharp public abstract class BaseTest { readonly Platform _platform; protected BaseTest(Platform platform) => _platform = platform; protected IApp App { get; private set; } [SetUp] public virtual void TestSetup() => App = AppInitializer.StartApp(_platform); } ``` In `BaseTest.cs`, create a property with a for each page class using the following format: `protected [Page Class Type] [Page Object Name] { get; private set; }` Here's how my `BaseTest.cs` looks so far: ```csharp public abstract class BaseTest { readonly Platform _platform; protected BaseTest(Platform platform) => _platform = platform; protected IApp App { get; private set; } protected PersonListPage PersonListPage { get; private set; } protected AddPersonPage AddPersonPage { get; private set; } [SetUp] public virtual void TestSetup() { App = AppInitializer.StartApp(_platform); } } ``` Now, in `TestSetup()`, let's initialize each page object. Here's how my `BaseTest.cs` looks now: ```csharp public abstract class BaseTest { readonly Platform _platform; protected BaseTest(Platform platform) => _platform = platform; protected IApp App { get; private set; } protected PersonListPage PersonListPage { get; private set; } protected AddPersonPage AddPersonPage { get; private set; } [SetUp] public virtual void TestSetup() { App = AppInitializer.StartApp(_platform); PersonListPage = new PersonListPage(App, PageTitles.PersonListPage); AddPersonPage = new AddPersonPage(App, PageTitles.AddPersonPage); } } ``` That's it for our BastTest! ## 6\. Create UI Tests Finally! Now it's time to write our UI tests! Let's modify `Test.cs` to inherit from `BaseTest`, and remove unneeded code that came from the template: ```csharp using System.Linq; using NUnit.Framework; using Xamarin.UITest; namespace CosmosDbSampleApp.UITests { [TestFixture(Platform.Android)] [TestFixture(Platform.iOS)] public class Tests : BaseTest { public Tests(Platform platform) : base(platform) { } } } ``` Now let's write our first test! NUnit will recognize any `public void` method that contains the `[Test]` attribute. I've added a Test called `public void AddNewContact()`. Take a look at my code below to understand how to customize your Test. **ProTip**: Use `//Arrange` `//Act` `//Assert` comments to ensure your tests follow best-practices and good organization. **ProTip**: NUnit also allows us to use `public Task` and `public async Task` methods if we need to run asynchronous code. ```csharp using System.Linq; using NUnit.Framework; using Xamarin.UITest; namespace CosmosDbSampleApp.UITests { [TestFixture(Platform.Android)] [TestFixture(Platform.iOS)] public class Tests : BaseTest { public Tests(Platform platform) : base(platform) { } [Test] public void AddNewContact() { //Arrange const string name = "Test Contact"; const int age = 37; //Act PersonListPage.TapAddButton(); AddPersonPage.EnterName(name); AddPersonPage.EnterAge(age); AddPersonPage.TapSaveButton(); PersonListPage.WaitForPageToLoad(); //Assert Assert.IsTrue(App.Query(name).Any()); Assert.IsTrue(App.Query(age.ToString()).Any()); } } } ``` ### Traveler: Prague, June 2018 URL: https://codetraveler.io/2018/06/26/traveler-prague/ Last updated: 2018-08-20T19:18:03.000Z I recently finished an amazing trip to Prague (known as Praha by the Czech) where I had am amzing opportunity to speak at the [Prague Azure User Group](https://www.meetup.com/Azure-User-Group/?ref=codetraveler.io) and [mDevCamp 2018](https://www.codetraveler.io/mdevcamp2018/?ref=codetraveler.io), and also joined the [.NET CZ Podcast](bit.ly/netczpodcast) for an episode! Prague is a beautiful city with amazingly kind people. If you ever have a chance to visit, go! # Hotel I stayed at the [Sheraton Prague Charles Square Hotel](https://www.starwoodhotels.com/preferredguest/property/overview/index.html?propertyID=3268&ref=codetraveler.io). It's a great location in the heart of downtown Prague. How was the coding you ask? - Internet Speed ⭐️⭐️⭐️⭐️ - Not bad for a hotel! I consistently hit download speeds of 20Mbps, which is great for day-to-day coding - Internet Reliability ⭐️⭐️⭐️⭐️⭐️ - Solid. No complaints here! - Comfort ⭐️⭐️⭐️ - There's room for improvement here. The desk was not ergonomic and the lone living room chair in the corner was simply unconfortable. I spent most of my coding time sitting on the bed. - Noise ⭐️⭐️⭐️⭐️ - No complaints! I had an interior-facing room, away from the street and I never heard any outside noise. - General Ambiance ⭐️⭐️⭐️⭐️ - This is a quaint hotel in a historic building. Similar to the rest of Prague, don't expect large rooms and high ceilings, but the Sharaton does the most with the space available. ![Czech Republic Flag](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/IMG_4381.JPG) ![Pork Knuckle at Pivovar U Tří růží](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/IMG_1850.JPG) ![King Wenceslas](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/IMG_1975.JPG) ![Old Building](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/IMG_2036.JPG) ![My Hosts](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/IMG_6011.JPG) ![Prague Castle with mDevCamp Speakers](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/IMG_6719.jpg) ![mDevCamp Speaker Gifts](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/06/IMG_8509.JPG) ### Walkthough: Creating UI Tests for Xamarin.Forms Apps, Part 1 URL: https://codetraveler.io/2018/05/22/creating-uitests-for-xamarin-forms-apps-part-1/ Last updated: 2018-08-20T19:18:03.000Z - [Code](#code) - [Traveler](#traveler) # Code Hey XamFam! Today we'll show how to add a few simple UI Tests to a simple Xamarin.Forms app. ## Why UI Tests? UI Tests are critical for maintaining our apps. We need to make sure we test every feature, both new and old, before publishing our apps, and the time it takes to test our apps increases exponentially as we add more features. UI Tests help to automate our testing cycle to help shrink our development cycles and publish our apps more quickly. Let's get started! ## Hello World UI Test Let's start with the Hello World app in Xamarin.Forms. ### Step 1\. Create File -> New Xamarin.Forms App Open Visual Studio. I'll be using Visual Studio for Mac. Select File -> New Solution ![1_New_Solution](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/1_New_Solution.png) Select Multiplatform -> App -> Blank Xamarin.Forms App -> Next ![2_Blank_Xamarin_Forms_App](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/2_Blank_Xamarin_Forms_App.png) Enter the App Name, then click Next. I'm naming mine "HelloWorld" ![3_App_Name](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/3_App_Name.png) On the next screen, make sure "Add an automated UI test project" is checked, then click "Create" ![4_Add_UITests](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/4_Add_UITests.png) ## Step 2\. Update Some NuGet Packages Update the Xamarin.UITest NuGet Package and the Xamarin.TestCloud.Agent NuGet Package ![5_Update_iOS_Test_NuGet](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/5_Update_iOS_Test_NuGet.png) ![5_Update_XamarinUITest_NuGet](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/5_Update_XamarinUITest_NuGet.png) ## Step 3\. Disable Android Mono Shared Runtime Xamarin.Android apps require the Mono runtime to be installed every time the our Xamarin.Android app is deployed to a device. To shorten build/deploy times, Visual Studio can install Mono onto the device once in the form of an `.apk` file, and allow future Xamarin.Android apps to leverage the existing Mono "app". While this is great and can save precious seconds when building/deploying our app during development, UI Tests aren't compatible with the Shared Mono Runtime, so let's disable it! Right-click on the Android project and select Options ![14_Android_Options](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/14_Android_Options.png) In the Project Options window, select Android Build and uncheck "Use Shared Mono Runtime" ![15_Shared_Runtime](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/15_Shared_Runtime.png) ## Step 4\. Add Automation.ID to Xamarin.Forms UI Every `Xamarin.Forms.Element` has a `string` property called [AutomationId](https://developer.xamarin.com/api/property/Xamarin.Forms.Element.AutomationId/?ref=codetraveler.io). This property is completely invisible to the user, and we can leverage it to uniquely identify every UI element that appears on the screen. Let's assign `AutomationId` for the `Label` in our Hello World Xamarin.Forms app. I named mine `"HelloWorldLabel"`. ```csharp public class App : Application { public App() { // The root page of your application var content = new ContentPage { Title = "HelloWorld", Content = new StackLayout { VerticalOptions = LayoutOptions.Center, Children = { new Label { HorizontalTextAlignment = TextAlignment.Center, Text = "Welcome to Xamarin Forms!", AutomationId = "HelloWorldLabel" }, } } }; MainPage = new NavigationPage(content); } } ``` ![6_Add_AutomationId](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/6_Add_AutomationId.png) That's it for the app! Now let's write a UI Test! ## Step 5\. Write UI Test Open `Tests.cs` in the UITest Project. Let's first add a `Using` statement to make our lives easier: ```csharp using Query = System.Func; ``` Now let's edit the existing test, `WelcomeTextIsDisplayed()`: ```csharp [Test] public void WelcomeTextIsDisplayed() { // Create a Xamarin.UITest that will search for our Label based on its AutomationId Query helloWorldLabelQuery = x => x.Marked("HelloWorldLabel"); // Perform the Query. // `app.Query` will return an Array all UI Elements that use "HelloWorldLabel" AppResult[] helloWorldLabelQueryResults = app.Query(helloWorldLabelQuery); // Because we've only assigned "HelloWorldLabel" to one UI Element, we are confident that the first result in the string helloWorldLabelText = helloWorldLabelQueryResults?.FirstOrDefault()?.Text; // `Assert.AreEqual` tells Xamarin.UITest to compare the expected string, "Welcome to Xamarin Forms!", with the actual string in helloWorldLabelText // If the strings are equal, our test will pass. // If the strings are not equal, our test will fail. Assert.AreEqual("Welcome to Xamarin Forms!", helloWorldLabelText); } ``` ![7_Write_UITest](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/7_Write_UITest.png) ## Step 6\. Build the Solution Select Build -> Build All ![8_Build_Solution](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/8_Build_Solution.png) ## Step 7\. Open the Test Results Pad ![9_Open_Test_Results_Pad](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/9_Open_Test_Results_Pad.png) ## Step 8\. Run the UI Test Open the Unit Test Pad (I know what you're thinking - "We're writing UI Tests! Why are we opening the Unit Tests pad?!". I don't have a good answer. For now, let's just call it an unfortunate name) View -> Pads -> Unit Tests ![10_Unit_Test_Pad](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/10_Unit_Test_Pad.png) In the Unit Tests Pad (I know...the name...ugghh), right-click on Test Apps, and select Add App Project ![11_Add_Test_Apps](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/11_Add_Test_Apps.png) In the Add App Project window, ensure both the iOS and Android projects are selected, then click OK ![12_Select_App_Projects](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/12_Select_App_Projects.png) That's it! Now, click Run All! ![13_Run_All](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/13_Run_All.png) ![RunUITestRecording](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/RunUITestRecording.gif) --- # Traveler ## Where am I? I am currently on Southwest Flight #478 from San Diego to Tampa. My wife and I are off to see a good friend is get married, and I'm more than happy to fly across the country to watch this guy marry his dream girl! ## How's the Internet Speed? ## ⭐️️️️️️️⭐️️⭐️ Don't get me wrong, I'm not going to be streaming 4k on this airplane's internet connection, but it does everything I need for browsing the web and working on apps. In general, Southwest's WiFi is solid, and at a cost of $8, it's the best value I've found flying the friendly skies. ## How's the Internet Reliability? ## ⭐️️️️️️️⭐️️⭐️⭐️️️️️️️⭐️️ I've got to hand it to Southwest: the internet connection has been rock solid. I like to keep a constant ping running while on the plane, and it's had zero dropped packets 👊 ![Screen-Shot-2018-02-18-at-8.19.58-PM](https://storage.ghost.io/c/dc/c2/dcc2e43e-24f8-46bb-a899-9c446f3ef794/content/images/2018/02/Screen-Shot-2018-02-18-at-8.19.58-PM.png) ## How's the Comfort? ## ⭐️⭐️ Working on a 15" MacBook Pro on a plane always leaves something to be desired. The leg room is decent on Southwest, but elbow space is at a premium. Sitting on the aisle seat also means I get bumped once every 20 minutes as a person walks down the aisle (see what I did there - it's a wedding pun!). Oh, and the guy in front of me is reclining his seat, so that's fun. ## How's the Noise? ## ⭐️⭐️ No crying babies on this flight, but it's a plane with loud engines. Not even my Bose QC15s + Post Malone can drown out all this background noise. ## General Ambiance ️ ## ⭐️️⭐️ It's not my first choice in coding locations, but I like what Southwest is doing with their ambiance. They've installed multi-colored lights in their 737s which give off different hues. Right now the cabin is illuminated in soft blue + red light, and I kind of dig it. ## Overall Score: ️ ## ⭐️️️⭐️️ Will definitely fly Southwest again, but I'll take my home office over a cramped seat in economy every day.