I've got a play button that I'd like to toggle with a play icon and a pause icon.
<Button DockPanel.Dock="Left" Margin="4" Command="{Binding PlayPress}">
<Image Source="/Assets/play.png" Width="24" Height="24"/>
</Button>
I was hoping I would be able to do something like Source="{Binding CurrentPlayButtonIcon}", where CurrentPlayButtonIcon would be a string. Then I could specify a URL to an image, in the the accompanying *ViewModel class. But after trying that out, it didn't seem to work.
I did a little searching on the issue tracker, and saw using something called a "custom converter", but I'm a little confused on how I'm supposed to implement it in my program. Is there simple (and complete) example of it? Or is there an easier way to saw out the images?
It took me a couple of days to track down the issues, try different things, trawl the documentation, etc. and I figured it out.
Firstly, make this class:
/// <summary>
/// <para>
/// Converts a string path to a bitmap asset.
/// </para>
/// <para>
/// The asset must be in the same assembly as the program. If it isn't,
/// specify "avares://<assemblynamehere>/" in front of the path to the asset.
/// </para>
/// </summary>
public class BitmapAssetValueConverter : IValueConverter
{
public static BitmapAssetValueConverter Instance = new BitmapAssetValueConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
if (value is string rawUri && targetType == typeof(IBitmap))
{
Uri uri;
// Allow for assembly overrides
if (rawUri.StartsWith("avares://"))
{
uri = new Uri(rawUri);
}
else
{
string assemblyName = Assembly.GetEntryAssembly().GetName().Name;
uri = new Uri($"avares://{assemblyName}{rawUri}");
}
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
var asset = assets.Open(uri);
return new Bitmap(asset);
}
throw new NotSupportedException();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
This class was adapted from https://github.com/VitalElement/AvalonStudio.Shell/blob/master/src/AvalonStudio.Shell.Extensibility/Converters/BitmapValueConverter.cs.
Go to your XAML file.
In your user control, you want to do something like this:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
...
xmlns:ext="clr-namespace:Emotion.Arcadia.Extensions;assembly=Emotion.Arcadia.Extensions"
...
x:Class="Emotion.Arcadia.Views.PlayerView">
Notice this line xmlns:ext="clr-namespace:Emotion.Arcadia.Extensions;assembly=Emotion.Arcadia.Extensions", add that line (not the rest of the stuff) as an attribute to your user control and replace the "Emotion.Arcadia.Extensions" with whatever the assembly name that has your value converter class is.
Then put this in your XAML code inside the user control tag.
<UserControl.Resources>
<ext:BitmapAssetValueConverter x:Key="variableImage"/>
</UserControl.Resources>
Now on your image, define your source like this.
<Image Width="75"
Height="73"
Source="{Binding PlaySource, Converter={StaticResource variableImage}}">
Congratulations, you can now change your PlaySource or whatever you named your binding property to any kind of path and it will change the image that is shown automatically.
This really should have been in documentation, or, ideally, it would be really nice if binding a string property inside a source implicitly used a converter like this. And the strangest thing? It technically _already does that_. I mean, the way you even use images _is by specifying a path._ I spent about an hour looking around the source trying to figure out how it does it, but without previous development experience (I started Avalonia a few days ago) it's like finding a needle in a haystack.
Probably worth mentioning this makes about 3 issues in total
https://github.com/AvaloniaUI/Avalonia/issues/3774
https://github.com/AvaloniaUI/Avalonia/issues/853
And now this one of course https://github.com/AvaloniaUI/Avalonia/issues/3860
I actually added the the method that you outlined above in my code. If this seems to be commonly desired thing (and it is something that's done in other UI toolkits), it might be best to just include this already in Avalonia.
hmm.. i approached this differently in my music player https://github.com/jmacato/Synfonia/
basically i just have the 2 images bound and one with a negation like:
<Button Name="PlayButton" HorizontalAlignment="Center" Width="36" Command="{Binding PlayCommand}">
<Panel>
<DrawingPresenter Drawing="{DynamicResource Play}" IsVisible="{Binding !IsPlaying}" Width="20"
Height="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
<DrawingPresenter Drawing="{DynamicResource Pause}" IsVisible="{Binding IsPlaying}" Width="20"
Height="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Panel>
</Button>
but the approach that @rollersteaam made is a good one too..
I suppose it'd be good to make a string/uri to bitmap source builtin, i'll perhaps try to add one in the next release
I do something similar to @jmacato but use a ToggleButton and use styles to show/hide based on the checked psudeo class.
@ahopper This sounds like it might be pretty neat? Could you share any code related to that please?
hmm.. i approached this differently in my music player https://github.com/jmacato/Synfonia/
basically i just have the 2 images bound and one with a negation like:<Button Name="PlayButton" HorizontalAlignment="Center" Width="36" Command="{Binding PlayCommand}"> <Panel> <DrawingPresenter Drawing="{DynamicResource Play}" IsVisible="{Binding !IsPlaying}" Width="20" Height="20" VerticalAlignment="Center" HorizontalAlignment="Center" /> <DrawingPresenter Drawing="{DynamicResource Pause}" IsVisible="{Binding IsPlaying}" Width="20" Height="20" VerticalAlignment="Center" HorizontalAlignment="Center" /> </Panel> </Button>
@jmacato Thanks for this! This looks way closer to the way I'd ideally like it to be and is pretty genius, never thought about it like this. I way prefer having stylistic definitions (like which image to use) inside style code, like XAML, declaratively like this. I had planned on making a custom control just so that I wouldn't have to manage binded image path strings in code.
This may even be better potentially since this might remove the need to retrieve images based off of changes in the binding property, which is much slower compared to changing visibility.
@jmacato Also wanted to ask, what's your rationale behind using DrawingPresenter over Image out of interest?
I've created a PR for the documentation to add information about the declarative approach shown by @jmacato and how binding to the Image's Source attribute does not work without use of a binding converter.
https://github.com/AvaloniaUI/avaloniaui.net/pull/169
This is a temporary fix for newcomers that want to make even something as basic as a button that changes images based on its state. Ideally binding to Source and having it work is the perfect behaviour, however having knowledge of a declarative approach, which I would argue is actually better, is good too.
I use DrawingPresenter so I can use vector icons, the 0.10 release of Avalonia lets you display these in the image control so DrawingPresenter will go.
my style is
<Style Selector="ToggleButton DrawingPresenter.tbchecked">
<Setter Property="IsVisible" Value="false"/>
</Style>
<Style Selector="ToggleButton:checked DrawingPresenter.tbchecked">
<Setter Property="IsVisible" Value="true"/>
</Style>
<Style Selector="ToggleButton DrawingPresenter.tbunchecked">
<Setter Property="IsVisible" Value="true"/>
</Style>
<Style Selector="ToggleButton:checked DrawingPresenter.tbunchecked">
<Setter Property="IsVisible" Value="false"/>
</Style>
and a button, not using tbunchecked in this case
<ToggleButton Classes="vtrx" IsChecked="{Binding Path=vtrx.muted}" ToolTip.Tip="stop audio" >
<Panel>
<DrawingPresenter Drawing="{DynamicResource Icon.Speaker}" />
<DrawingPresenter Width="14" Height="14" Margin="14,14,0,0" Drawing="{DynamicResource Icon.SpeakerMute}" Classes="tbchecked"/>
</Panel>
</ToggleButton>
Ahh! Vector icons! Wonderful! Things are looking up. Thank you for sharing, this looks great.
There are a few vector icons here https://github.com/ahopper/Avalonia.IconPacks
Okay, if this alternative method lets mu use my SVGs, I think I may be sold on it instead of the on that AvalonStudio provided.
Whoops! Didn't realize that SVGs aren't supported yet.
@define-private-public yes, hopefully we can do something about it in the next next release ... but you can use the paths on svg's or export them as xaml in Inkscape
Most helpful comment
I do something similar to @jmacato but use a ToggleButton and use styles to show/hide based on the
checkedpsudeo class.