IMarkupExtension with bindable properties

681 Views Asked by At

I've created an IMarkupExtension for an ImageSource which gets a specified symbol from a specified font and displays it in a specified color with a specified height. Most of the times the icon name is static and I write into the XAML directly. But sometimes there are lists of things that have a property which determines which icon should be used. For this case it is necessary that the icon name is bindable.

Here is (more or less) the current state of my FontImageExtension:

[ContentProperty(nameof(IconName))]
public class FontImageExtension : IMarkupExtension<ImageSource>
{
    private readonly IconFontService iconFontService;

    [TypeConverter(typeof(FontSizeConverter))]
    public double Size { get; set; } = 30d;

    public string IconName { get; set; }

    public Color Color { get; set; }

    public string FontFamily { get; set; }

    public FontImageExtension()
    {
        iconFontService = SomeKindOfContainer.Resolve<IconFontService>();
    }

    public ImageSource ProvideValue(IServiceProvider serviceProvider)
    {
        if (string.IsNullOrEmpty(IconName))
            return null;

        IconFont iconFont = iconFontService.GetIconFont();

        if (iconFont == null)
            return null;

        string glyphCode = iconFont.GetGlyphCode(IconName);

        if (string.IsNullOrEmpty(glyphCode))
            return null;

        FontImageSource fontImageSource = new FontImageSource()
        {
            FontFamily = iconFont.GetPlatformLocation(),
            Glyph = glyphCode,
            Color = this.Color,
            Size = this.Size,
        };

        return fontImageSource;
    }

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
    {
        return ProvideValue(serviceProvider);
    }
}

Most of the time I use it like this in XAML (which already works perfectly):

<Image Source="{m:FontImage SomeIcon, Color=Black, Size=48}"/>

But for dynamic UI (e.g. lists or something) I need it like this:

<CollectionView ItemsSource={Binding SomeCollection}">
    <CollectionView.ItemTemplate>
        <StackLayout>
            <Image Source="{m:FontImage IconName={Binding ItemIcon}, Color=Black, Size=48}"/>
            <Label Text="{Binding ItemText}"/>
        </StackLayout>
    </CollectionView.ItemTemplate>
</CollectionView>

How can I make this work?

3

There are 3 best solutions below

2
On BEST ANSWER

I solved this problem by creating a converter (like @Leo Zhu suggested) but in addition to the IMarkupExtension. So my extension stays as is (with the addition of a constant value that gets used in the converter) and the code for the converter is as follows:

public class FontIconConverter : IValueConverter, IMarkupExtension
{
    private IServiceProvider serviceProvider;

    public Color Color { get; set; }

    [TypeConverter(typeof(FontSizeConverter))]
    public double Size { get; set; } = FontIconExtension.DefaultFontSize;

    public string FontFamily { get; set; }

    public FontIconConverter()
    {
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is string iconName))
            return null;

        var fontIcon = new FontIconExtension()
        {
            IconName = iconName,
            Color = Color,
            Size = Size,
            FontFamily = FontFamily,
        };

        return fontIcon.ProvideValue(serviceProvider);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
        return this;
    }
}

It then can be used like this:

<Image Source="{Binding IconNameProperty, Converter={c:FontIconConverter Color=Black, Size=48}}"/>

And for static values it stays like this:

<Image Source="{m:FontImage SomeIconsName, Color=Black, Size=48}"/>
0
On

It seems you could not use IMarkupExtension with bindable properties .As a 'Binding' can only be set on BindableProperty of a BindableObject.The problem is that MarkupExtension class does not derive from BindableObject, that's why it is not possible to set binding on it's properties.Though you let it implement BindableObject,it still could not work.

A workaround is using Value Converters.

For example:

class ImageSourceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var p = parameter.ToString().Split('|');
        string colorName = p[0];
        ColorTypeConverter colorTypeConverter = new ColorTypeConverter();
        Color color = (Color)colorTypeConverter.ConvertFromInvariantString(colorName);
        double fontSize = double.Parse(p[1]);

        //didn't test this here.
        IconFontService iconFontService = SomeKindOfContainer.Resolve<IconFontService();
        IconFont iconFont = iconFontService.GetIconFont();
        if (iconFont == null)
            return null;

        string glyphCode = iconFont.GetGlyphCode((string)value);
        if (string.IsNullOrEmpty(glyphCode))
            return null;

        FontImageSource fontImageSource = new FontImageSource()
        {
            FontFamily = iconFont.GetPlatformLocation(),
            Glyph = glyphCode,
            Color = color,
            Size = fontSize,
        };
        return fontImageSource;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

use in your xaml:

<ContentPage.Resources>
    <ResourceDictionary>
        <local:ImageSourceConverter x:Key="imageConvert" />
    </ResourceDictionary>
</ContentPage.Resources>

<CollectionView ItemsSource={Binding SomeCollection}">
  <CollectionView.ItemTemplate>
    <StackLayout>
        <Image Source="{Binding Name,Converter={StaticResource imageConvert}, ConverterParameter=Color.Black|48}"/>
        <Label Text="{Binding ItemText}"/>
    </StackLayout>
  </CollectionView.ItemTemplate>
</CollectionView>

Also see failed attempt declaring BindableProperty: IMarkupExtension with bindable property does not work and a more ambitious approach to a somewhat different situation - might be relevant: MarkupExtension for binding.

0
On

You can actually make your FontImageExtension markup extension implement (1) BindableObject, (2) IMarkupExtension, AND IMultiValueConverter all at the same time:


[ContentProperty(nameof(IconName))]
public class FontImageExtension : BindableObject, IMarkupExtension<BindingBase>, IMultiValueConverter
{
    private readonly IconFontService iconFontService;

    public static readonly BindableProperty SizeProperty
        = BindableProperty.Create(nameof(Size), typeof(double), typeof(FontImageExtension));
    public static readonly BindableProperty IconNameProperty
        = BindableProperty.Create(nameof(IconName), typeof(string), typeof(FontImageExtension));
    public static readonly BindableProperty ColorProperty
        = BindableProperty.Create(nameof(Color), typeof(Color), typeof(FontImageExtension));
    public static readonly BindableProperty FontFamilyProperty
        = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(FontImageExtension));

    [TypeConverter(typeof(FontSizeConverter))]
    public double Size
    {
        get { return (double)GetValue(SizeProperty); }
        set { SetValue(SizeProperty, value); }
    }

    public string IconName
    {
        get { return (string)GetValue(IconNameProperty); }
        set { SetValue(IconNameProperty, value); }
    }

    public Color Color
    {
        get { return (Color)GetValue(ColorProperty); }
        set { SetValue(ColorProperty, value); }
    }

    public string FontFamily
    {
        get { return (string)GetValue(IconNameProperty); }
        set { SetValue(FontFamilyProperty, value); }
    }


    public FontImageExtension()
    {
        iconFontService = SomeKindOfContainer.Resolve<IconFontService>();
    }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        return (this as IMarkupExtension<BindingBase>).ProvideValue(serviceProvider);
    }

    BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceProvider)
    {
        return new MultiBinding()
        {
            Converter = this,
            Mode = BindingMode.OneWay,
            Bindings = new Collection<BindingBase>
            {
                new Binding(nameof(Size), BindingMode.OneWay, null, null, null, this),
                new Binding(nameof(IconName), BindingMode.OneWay, null, null, null, this),
                new Binding(nameof(Color), BindingMode.OneWay, null, null, null, this),
                new Binding(nameof(FontFamily), BindingMode.OneWay, null, null, null, this)
            }
        };
    }

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double Size = (double)values[0];
        string IconName = (string)values[1];
        Color Color = (Color)values[2];
        string FontFamily = (string)values[3];

        if (string.IsNullOrEmpty(IconName))
            return null;

        IconFont iconFont = iconFontService.GetIconFont();
        if (iconFont == null)
            return null;

        string glyphCode = iconFont.GetGlyphCode(IconName);
        if (string.IsNullOrEmpty(glyphCode))
            return null;

        FontImageSource fontImageSource = new FontImageSource()
        {
            FontFamily = iconFont.GetPlatformLocation(),
            Glyph = glyphCode,
            Color = this.Color,
            Size = this.Size,
        };

        return fontImageSource;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}