views:

45

answers:

2

I have a wpf ListBox, and each item has an image that the list needs to download from a server - the list definition looks like so:

<ListBox x:Name="List" BorderThickness="0" AlternationCount="2" ItemContainerStyle="{StaticResource alternatingWithBinding}" 
     HorizontalContentAlignment="Stretch"  ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<ListBox.ItemTemplate>
    <DataTemplate>
        <Grid x:Name="itemsGrid" Margin="3" ShowGridLines="False" >
            <Grid.RowDefinitions>
                <RowDefinition Height="59"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="45" />
                <ColumnDefinition Width="60" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="150" />
            </Grid.ColumnDefinitions>

            <Button x:Name="btn" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center" Tag="{Binding}" 
                CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" />
            <Image x:Name="Thumb" Grid.Column="1" Stretch="Uniform" Opacity="1.0" Source="{Binding Path=Image, Converter={StaticResource ImageConverter}}" Height="65" VerticalAlignment="Center"/>
            <TextBlock x:Name="Name" Grid.Column="2" Padding="2" Margin="17,0" VerticalAlignment="Center" Text="{Binding Path=Name}"
                       Tag="{Binding}" />
        </Grid>
        <DataTemplate.Triggers>
            ...
        </DataTemplate.Triggers>
    </DataTemplate>
</ListBox.ItemTemplate>

and the converter looks like:

    public class ImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
            if (value is string)
            {
                value = new Uri((string)value);
            }

            if (value is Uri)
            {
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.DecodePixelWidth = 150;
                bi.UriSource = value;
                bi.DownloadFailed += new EventHandler<ExceptionEventArgs>(bi_DownloadFailed);
                bi.EndInit();
                return bi;
            }

            return null;
    }

The idea is to show a default image when the sourceUrl returns nothing from the server. But since i'm using the converter in the XAML code,

Source="{Binding Path=Image, Converter={StaticResource ImageConverter}}"

I'm not sure how to intercept that case. I see that BitmapImage has the DownloadFailed event which is perfect for me, I just don't know how to use that in this context.

A: 

Hmmm.... I believe download occurs on EndInit, which matters. Since you're using a static resource for a converter, you end up having a single instance per application. That means if you create a variable scoped to the class-level and use that to store the results of the download, you can have issues if you aren't careful with the state of your variables between calls to Convert. However, since the UI is rendered in a single thread, you don't have to worry too much.

public class ImageConverter : IValueConverter
{
    private static readonly BitmapImage _default = MakeDefault();
    private bool _downloadFailed;

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
        if (value is string)
        {
            value = new Uri((string)value);
        }

        if (value is Uri)
        {
            _downloadFailed = false;
            BitmapImage bi = new BitmapImage();
            bi.BeginInit();
            bi.DecodePixelWidth = 150;
            bi.UriSource = value;
            // the event handler sets _downloadFailed to true!
            bi.DownloadFailed += bi_DownloadFailed;
            bi.EndInit();
            // unhook so we don't unintentionally keep the instance alive
            // this is important; you will leak BI instances otherwise!
            bi.DownloadFailed -= bi_DownloadFailed;
            if(_downloadFailed)                
              return _default;
            return bi;
        }

        return null;
}
Will
Thanks. Using your code, the DownloadFailed event doesn't get fired at all. It did fire when I only have the event hooked (without unhooking it), but the problem I was having is that by the time it fired, execution was already passed the Convert method. When looking at the object in the handler's first parameter, I get the actual url that failed, but I dont know how to bind that to the right item in the list.
Also, If I don't use the converter (which I must do for a different reason), then the ImageFailed event fires for that item and setting the default image in that handler works. Is the fact that i'm using a converter prevent the list item ImageFailed event to fire?
Sorry, for some reason this didn't fire before. It does now, and has nothing to do with the converter.
A: 

Have you looked at the Binding class TargetNullValue property?

I'd have the Converter return null if you can't download the file.


 public object Convert(object value, Type targetType, 
  object parameter, System.Globalization.CultureInfo culture)
{

  if (parameter.ToString()=="blue")
  {
    return new Uri("Butterfly1.png", UriKind.RelativeOrAbsolute);
  }
  return null;
}

Then setup a default image in the XAML

<Window.Resources>
<my:ImageConverter x:Key='ImageConverter1' />
<BitmapImage x:Key='defaultImage'
             UriSource='/WpfApplication1;component/default.png' />


Then in your binding specify the TargetNullValue.


 <Image   Source='{Binding Converter={StaticResource ImageConverter1},
  ConverterParameter="red",TargetNullValue={StaticResource defaultImage}}'
         Height='100' />
<Image   Source='{Binding Converter={StaticResource ImageConverter1},
  ConverterParameter="blue",TargetNullValue={StaticResource defaultImage}}'
         Height='100' />

Walt Ritscher