Search code examples
xamlmaui

How to load external (non embedded) images in .NET MAUI XAML?


I have a problem figuring out how to load images in XAML where the image file is not embedded in the application itself. I cannot use C# for this as the XAML files are dynamically loaded and parsed at runtime. So you cannot hook on a method or event to load the image source from C#. It has to be done in XAML.

As soon as my application starts, it downloads the latest resources from a web server:

private static void DownloadResourcesFromWebServer(string url)
{
    try
    {
        string finalPath = Path.Combine(Microsoft.Maui.Storage.FileSystem.AppDataDirectory, "resources_tmp.zip");
        if (File.Exists(finalPath))
        {
              File.Delete(finalPath);
        }

        using HttpClient client = new HttpClient();
        using HttpResponseMessage response = client.GetAsync(url).Result;
        using HttpContent content = response.Content;
        using Stream stream = content.ReadAsStreamAsync().Result;
        using FileStream fs = new FileStream(finalPath, FileMode.OpenOrCreate, FileAccess.Write);

        stream.CopyTo(fs);
        fs.Flush();
        fs.Close();

        ZipFile.ExtractToDirectory(finalPath, Microsoft.Maui.Storage.FileSystem.AppDataDirectory);
    } catch(Exception ex)
    {
        Logger.Log(ex);
        Debugger.Break();
    }
}

As you can see, all my resources are exported to the Microsoft.Maui.Storage.FileSystem.AppDataDirectory path.

But I cannot seem to get this to work. I cannot define the source of the images to be loaded from this path.

Am I missing something? Am I using the wrong path to save my resources (images only in this case)?

I have tried to access the downloaded images with a test <Image> tag in a XAML file. I've tried the following configurations (all without success)

<Image Source="files/NAME.png" />
<Image Source="NAME.png" />
<Image>
    <Image.Source>
        <FileImageSource File="files/NAME.png"></FileImageSource>
    </Image.Source>
</Image>

and

<Image>
    <Image.Source>
        <FileImageSource File="NAME.png"></FileImageSource>
    </Image.Source>
</Image>

PS: The project is NOT a Blazor App


Solution

  • To answer my own question:

    I followed Jason's advice to do it via data binding.

    To dynamically get the correct resource path (which will be different on iOS and Android), you can simply use a custom ValueConverter.

    To do this, you'll first need to define it as a class like this:

    public class FrameworkPathConverter : IValueConverter
    {
        /// <summary>
        /// Converts a path to a full qualified, platform specific path.
        /// </summary>
        /// <param name="parameter">The path of the file to be qualified</param>
        /// <returns></returns>
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (parameter is string fileName)
            {
                return Path.Combine(Microsoft.Maui.Storage.FileSystem.AppDataDirectory, fileName);
            }
            return value;
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
    }
    
    

    Then you need to register it globally in your application (Resources) like this:

    ?xml version="1.0" encoding="utf-8" ?>
    <Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="AppFramework.App"
                 xmlns:conv="clr-namespace:AppFramework.Converter">
                 
        <Application.Resources>
            <ResourceDictionary>
                <!-- ... -->
    
                <!-- Custom Converter -->
                <conv:FrameworkPathConverter x:Key="FrameworkPathConverter" />
                
                <!-- ... -->
            </ResourceDictionary>
        </Application.Resources>
    </Application>
    

    You may need to change the xmlns:conv="clr-namespace:AppFramework.Converter" to match your namespace and the <conv:FrameworkPathConverter x:Key="FrameworkPathConverter" /> to match your class name.

    Then you can use it to display downloaded and extracted images like this:

    <Image Source="{Binding Converter={StaticResource FrameworkPathConverter}, ConverterParameter=logo_wide.jpg}"></Image>
    

    You simply pass the created converter to the Converter (as a static resource) and use the name of the file you want to convert to a full path as the `ConverterParameter'.

    With some modifications to the Convert() method in the FrameworkPathConverter class, you could use different paths if needed.

    I'm not sure this is the most optimal, efficient and convenient way of doing this, but for now it's working as expected.

    I'd still be happy to get other answers that can solve this problem.