pax
pax

Reputation: 1903

Xamarin: add image to my button from PCL (not from Resources)

I'm working on a Xamarin.Forms project utilizing PCL (not the shared project).
I have a few images in my Resources folders in both Android and iOS project.

This works and the icons show in buttons as they're supposed to:

<Button Image="speaker.png" Grid.Row="0" Grid.Column="0" />

I also have a folder in my PCL project with some images: images/icons/speaker.png
I've tried this:

<Button Image="{local:EmbeddedImage TestThree.images.icons.speaker.png}" />

...but that didn't work...

I would like those buttons to show images from my images folder in my PCL project.
So my question would be...

<Button WHAT GOES HERE? Grid.Row="0" Grid.Column="0" />

Upvotes: 1

Views: 2574

Answers (2)

Petr Voborn&#237;k
Petr Voborn&#237;k

Reputation: 1265

When Button.Image wants FileImageStream, I give it to him. But as images in the project I still use embedded resources PNG files in PCL (or .NET standard 2.0) library (project). For example the PCL project name is "MyProject" and I have an image placed in its subfolder "Images\Icons\ok.png". Then the resource name (e.g. for ImageSource.FromResource) is "MyProject.Images.Icons.ok.png". This method copies embedded resource file into the file in application local storage (only first time).

public static async Task<string> CopyIcon(string fileName)
{
    if (String.IsNullOrEmpty(fileName)) return "";
    try
    {
        // Create (or open if already exists) subfolder "icons" in application local storage
        var fld = await FileSystem.Current.LocalStorage.CreateFolderAsync("icons", CreationCollisionOption.OpenIfExists);
        if (fld == null) return ""; // Failed to create folder

        // Check if the file has not been copied earlier
        if (await fld.CheckExistsAsync(fileName) == ExistenceCheckResult.FileExists)
            return (await fld.GetFileAsync(fileName))?.Path; // Image copy already exists

        // Source assembly and embedded resource path
        string imageSrcPath = $"MyProject.Images.Icons.{fileName}"; // Full resource name
        var assembly = typeof(FileUtils).GetTypeInfo().Assembly;

        // Copy image from resources to the file in application local storage
        var file = await fld.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists);
        using (var target = await file.OpenAsync(PCLStorage.FileAccess.ReadAndWrite))
        using (Stream source = assembly.GetManifestResourceStream(imageSrcPath))
            await source.CopyToAsync(target); // Copy file stream

        return file.Path; // Result is the path to the new file
    }
    catch 
    {
        return ""; // No image displayed when error
    }
}

When I have a regular file, I can use it for the FileImageStream (Button.Image). The first option is use it from the code.

public partial class MainPage : ContentPage
{
    protected async override void OnAppearing()
    {
        base.OnAppearing();
        btnOk.Image = await FileUtils.CopyIcon("ok.png"); 
    }   
}

Also I can use it in the XAML, but I must create an implementation of IMarkupExtension interface.

[ContentProperty("Source")]
public class ImageFileEx : IMarkupExtension
{
    public string Source { get; set; }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        return Task.Run(async () => await FileUtils.CopyIcon(Source)).Result;
    }
}

Now I can assign the image direct in the XAML.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MyProject"
             x:Class="MyProject.MainPage"
             xmlns:lcl="clr-namespace:MyProject;assembly=MyProject">
    <Grid VerticalOptions="Fill" HorizontalOptions="Fill">
        <Button Image="{lcl:ImageFileEx Source='ok.png'}" Text="OK" />
    </Grid>
</ContentPage>

For this solution the NuGet PCLStorage is needed.

Upvotes: 3

Lewis Kwong
Lewis Kwong

Reputation: 41

The reason that does not work is because the properties are bound to different types.

Button's Image property takes a "FileImageSource" - Github

Image's Source property takes a "ImageSource" - Github

From the local:EmbeddedImage im guessing you were using the extension from Xamarin forms image docs

That would not work because it loads a "StreamImageSource" instead of "FileImageSource".

In practice you should not do this as it would not load from different dimension images(@2x, xhdpi etc) and would give bad quality images and not support multiple resolutions.

You could use a view container(Stack layout etc) with a TapGestureRecognizer and an image centered inside it or create a custom renderer which really is more effort than its worth. None of these still would still obviously not handle multiple images though.

The correct solution would be to generate the correct size images from the base(Which I would assume would be MDPI Android or 1X for iOS) and put them in the correct folders and reference them in your working Button example.

Upvotes: 0

Related Questions