Handling image paths in Xamarin.Forms on UWP

Posted by on

C# Xamarin

Adding a Universal Windows Platform project to an existing Xamarin.Forms Android/iOS solution is meant to "just work". However when doing this myself I have found a few things that don't work as expected, one of those was images and image paths.

On iOS and Android there are special folders where the platform stores it's image files, iOS has Resources\Images.xcassets and Android has Resources\drawable.

So on either of these 2 platforms all you need to do to add an image in xaml is something like this: <Image Source="MyIcon" /> and it would pull an image from the relevant folder for you, you don't even have to put in the file extension.

I found on a UWP app this isn't the case, so I did a bit of searching and came across this on the official Xamarin.Forms guide for images:

  • iOS - Place images in the Resources folder with Build Action: BundleResource. Retina versions of the image should also be supplied - two and three times the resolution with a @2x or @3x suffixes on the filename before the file extension (eg. myimage@2x.png).
  • Android - Place images in the Resources/drawable directory with Build Action: AndroidResource. High- and low-DPI versions of an image can also be supplied (in appropriately named Resources subdirectories such as drawable-ldpi, drawable-hdpi, and drawable-xhdpi).
  • Windows Phone - Place images in the application's root directory with Build Action: Content.
  • Windows/UWP - Place images in the application's root directory with Build Action: Content.

The project I am working on has just over 200 images, so putting them in the project root is ridiculous!


The solution?

I created a custom renderer for the Image control on UWP and from there I could override the native image source and change the path of the image to one that suited me. This isn't a perfect solution but it's done the job so far, as long as all your images are in the same folder and have the png extension.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DkDevelopment.UWP;
using Xamarin.Forms;
using Xamarin.Forms.Platform.UWP;
using Xamarin.Forms.Internals;

[assembly: ExportRenderer(typeof(Image), typeof(FixedImageRenderer))]

namespace DkDevelopment.UWP
{
    // Image renderer based on The Xamarin.Forms UWP ImageRenderer
    // https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Platform.WinRT/ImageRenderer.cs
    // Ensures images are loaded from an Images folder in the project instead of the root and they have the .png extension which is required for UWP but not iOS or Android.
    public class FixedImageRenderer : ImageRenderer
    {
        bool _disposed;

        private string _imagePrefix = "Images\\";

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);

            _disposed = true;
        }

        protected override async Task TryUpdateSource()
        {
            // By default we'll just catch and log any exceptions thrown by UpdateSource so we don't bring down
            // the application; a custom renderer can override this method and handle exceptions from
            // UpdateSource differently if it wants to

            try
            {
                await UpdateSource2().ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Log.Warning(nameof(ImageRenderer), "Error loading image: {0}", ex);
            }
            finally
            {
                ((IImageController)Element)?.SetIsLoading(false);
            }
        }

        private void TryFixSourcePath(ImageSource source)
        {
            if (source is FileImageSource)
            {
                var fileSource = source as FileImageSource;
                if (fileSource != null)
                {
                    var filePath = fileSource.File;
                    if (!filePath.StartsWith(_imagePrefix))
                        filePath = _imagePrefix + filePath;
                    if (!filePath.EndsWith(".png"))
                        filePath += ".png";

                    if (filePath != fileSource.File)
                        fileSource.File = filePath;
                }
            }
        }

        protected async Task UpdateSource2()
        {
            if (_disposed || Element == null || Control == null)
            {
                return;
            }

            Element.SetIsLoading(true);

            ImageSource source = Element.Source;
            TryFixSourcePath(source);

            IImageSourceHandler handler;
            if (source != null && (handler = Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null)
            {
                Windows.UI.Xaml.Media.ImageSource imagesource;

                try
                {
                    imagesource = await handler.LoadImageAsync(source);
                }
                catch (OperationCanceledException)
                {
                    imagesource = null;
                }

                // In the time it takes to await the imagesource, some zippy little app
                // might have disposed of this Image already.
                if (Control != null)
                {
                    Control.Source = imagesource;
                }

                RefreshImage();
            }
            else
            {
                Control.Source = null;
                Element.SetIsLoading(false);
            }
        }

        void RefreshImage()
        {
            ((IVisualElementController)Element)?.InvalidateMeasure(InvalidationTrigger.RendererReady);
        }
    }
}

Get the full source here: https://gist.github.com/dkarzon/fe029bc1b3e31fcb9753855437241467