ImageFailed with GridView - c#

I have a problem with GridView and multiple items. Each of the item has image in it, source is online image, bound to property, like this:
<GridView x:Name="gridView" Width="710" ItemTemplate="{StaticResource FirstTemplate}" AllowDrop="True" CanDragItems="True" CanReorderItems="True">
<DataTemplate x:Key="FirstTemplate">
<Grid HorizontalAlignment="Left" Width="306" Height="210">
<Border Background="White" Opacity="0.1"/>
<Image Stretch="Uniform" Width="190" Height="100" Margin="0,50,0,0" ImageFailed="ImageFailed" Source="{Binding ImagePath}"/>
</Grid>
</DataTemplate>
Image paths are like this:
www.example.com/images/1.png
www.example.com/images/2.png
www.example.com/images/3.png
and so on...
If some image not exist, for example www.example.com/images/29.png, I use the event ImageFailed, which change the source of the image to image that is located in my project (default image). Code in this event:
private void ImageFailed(object sender, ExceptionRoutedEventArgs e)
{
var image = sender as Image;
image.Source = new BitmapImage(new Uri("ms-appx:///Images/default.png"));
}
And this is working just fine, the default image is shown in the items that don't have images. But, when I scroll down the gridview, and then return to the beginning, images are messed up. Some items that had their images, now have the default image. Again when I scroll the gridview, and then return, again random changes with images.
Is this some cache problem? What could be the problem here? Or is there any better way of setting the default image source?

The source of your problem could be virtualization, i.e. reuse of item containers. When you replace a failed image by a fallback image in your ImageFailed handler, you are effectively replacing the Binding by a fixed value, so that the item container will later always show only the fallback image.
You may instead implement the ImageFailed handler in the view model, so that replacing the image with a fallback image won't break the Binding.
Add another property, e.g. Image to your item class
public class ImageItem
{
public string ImagePath { get; set; }
private BitmapImage image;
public BitmapImage Image
{
get
{
if (image == null)
{
image = new BitmapImage();
image.ImageFailed += (s, e) =>
image.UriSource = new Uri("ms-appx:///Images/default.png");
image.UriSource = new Uri(ImagePath);
}
return image;
}
}
}
and change the Binding to this:
<Image ... Source="{Binding Image}"/> // no ImageFailed handler here

Related

Reordering ListView items in UWP messes up the content

I'm using ListViews to make a kanban list in UWP. As the picture below shows, re-ordering items a few times results in the content of one or some of them being wrong.
Further re-ordering will have the content going back and forth being correct and wrong and everything is back to normal when re-loading the page which means there's not data change but just the image control displaying the wrong image. ( It can happen with any other control too )
For reference, The images are local files which I'm loading in the Image control's Loaded event, and the ListView simply has CanReorderItems and AllowDrop set to true.
Here's how the XAML looks
<ListView x:Name="LView" MinWidth="240" Grid.Row="1" ItemsSource="{x:Bind List.Tasks}" ReorderMode="Enabled" CanReorderItems="True" AllowDrop="True" CanDragItems="True" SelectionMode="None" IsItemClickEnabled="True" ScrollViewer.VerticalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.IsVerticalRailEnabled="True" ItemClick="LView_ItemClick">
<ListView.ItemContainerStyle>
...
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="mongo:Task">
<Grid Padding="12 0" >
<Grid.RowDefinitions>
...
</Grid.RowDefinitions>
<Border CornerRadius="4 4 0 0" Margin="-12 0" >
<Image x:Name="Cover" MaxHeight="160" Stretch="UniformToFill" Tag="{x:Bind Attachments}" Loaded="Image_Loaded" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
</Border>
...
And here's the Loaded event
private async void Image_Loaded(object sender, RoutedEventArgs e)
{
var img = sender as Image;
if (img.Source is object) return;
var attachments = img.Tag as ObservableCollection<TaskAttachment>;
if (attachments is null) return;
var cover = attachments.Where(_a => _a.is_cover).FirstOrDefault();
if (cover is object && cover.type == "image")
{
var path = BrandboxSettings.Instance.server_path + "projects\\" + cover.path;
Output.WriteLine(path);
var file = await StorageFile.GetFileFromPathAsync(path);
using (IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read))
{
// Set the image source to the selected bitmap
BitmapImage bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(fileStream);
img.Source = bitmapImage;
}
}
}
Edit: It's worth noting that even if one of the cards does not initially have an image, reordering will cause it to have one.
Any help would be appreciated
Okay so I tried changing the ItemsPanel to a StackPanel and it seems to be working now.
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizationStackPanel/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
Edit:
It also seems to work by setting the Panel to VirtualizationStackPanel

How set the property of WPF DataGrid (mvvm) in "MouseDown" event based on "sender"?

I'm having some trouble to set one property (MouseDown event) value based on it's sender. I have "MyPhotoA" and "MyPhotoB" binded to an observableCollection. Both trigger the same event "MyOnClick" Here is the xaml:
... stuff
<DataTemplate>
<Image Source="{Binding MyPhotoA, UpdateSourceTrigger=LostFocus}" MouseDown="MyOnClick" />
</DataTemplate>
... stuff
<DataTemplate>
<Image Source="{Binding MyPhotoB, UpdateSourceTrigger=LostFocus}" MouseDown="MyOnClick" />
</DataTemplate>
... stuff
These two datatemplates are used for two datagridtemplatecolumns in the datagrid. Hence there are two columns of images and the user clicks one. I want to set the source on the image clicked.
The event "MyOnClick" is something like this:
private void MyOnClick(object sender, MouseButtonEventArgs e)
{
var myImage File.ReadAllBytes("c:\\MyImage.jpeg")
var dc = (sender as System.Windows.Controls.Image).DataContext;
MyModelClass itemSelected = (MyModelClass)dc;
itemSelected.PhotoA = myImage;//Setting PhotoA
itemSelected.PhotoB = myImage;//Setting PhotoB
//How to set the photo based on "sender" property? Like:
//sender.[somestuff]=myImage;
}
I'd like to use the same method to set data in PhotoA and PhotoB based on the sender property binded to it. So if user click in the "PhotoA" DataGrid cell, the image is setted to "PhotoA". If click is done in "PhotoB" then "PhotoB" data is setted.
!!!Note!!!: I don't want tricks like
If (sender.name="PhotoA") then
itemSelected.PhotoA = myImage;
else
itemSelected.PhotoB = myImage;
Thanks in advance
[Workaround Update]
I could not find the answer so I used a workaround:
1)edit xaml code, adding a property "name" to each Photo:
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Name="ImageMyPhotoA" Source="{Binding Photo}" MouseDown="MyOnClick" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
On the event, I manually added the bin to the the observable collection.
private void MyOnClick(object sender, MouseButtonEventArgs e)
{
var myImage = File.ReadAllBytes("c:\\MyImage.jpeg");
var dc = (sender as Image).DataContext;
MyModelClass itemSelected = (MyModelClass)dc;
var senderName = (sender as Image).Name;
if (senderName == "ImagePhotoA")
{
itemSelected.PhotoA = myImage;
}
if (senderName == "ImagePhotoB")
{
itemSelected.PhotoB = myImage;
}
}
Conclusion
Setting properties in "MouseDown" event based on Sender (Sender.[SomeSenderProperty] = "Something") seems not possible OR over complicated. I suggest to mark the sender's name in xaml (like the example). Thanks for the good fellows for your help, I really appreciate.
You're essentially trying to set the source property of an image the user clicked.
When you do that you want it to persist, presumably, and you probably won't want to overwrite the binding so make your binding twoway.
<Image Source="{Binding MyPhotoA, Mode=TwoWay}"
In your click handler.
Cast your sender to image.
var img = sender as Image;
(You should routinely null check when you do as anything.)
But this gives you a reference to the appropriate image control to work with.
Set the value.
As Clemens points out, I was overcomplicating this with:
img.SetCurrentValue(SourceProperty, Abitmapimage);
And you can just do:
img.Source = new BitmapImage(new Uri(#"C:\MyImage.jpeg"));

ImageFailed is called when scrolling in listbox

I have a listbox in which each item consists an image downloaded online.
<ListBox.ItemTemplate >
<DataTemplate>
<StackPanel Margin="10" >
<RelativePanel>
<Image ImageFailed="Image_ImageFailed">
<Image.Source>
<BitmapImage UriSource="{Binding IMG1}" />
</Image.Source>
private void Image_ImageFailed(object sender, ExceptionRoutedEventArgs e)
{
((Image)sender).Source = new BitmapImage(new Uri("ms-appx:///assets/StoreLogo.png"));
}
and this is how I bind the data;
data = from query in loadedData.Descendants("item") select new Models.Item
{
IMG1 = "https://example.png",
};
ItemsListBox.ItemsSource = data.Select(grp => grp.FirstOrDefault());
At first, it is working fine. However, when I start to scroll down and up again, all images get replaced by a default one as a result of ImageFailed method. So;
Why listbox tries to reload images when I scroll?
Why ImageFailed gets called even though image url is valid?
Do I have to cache images myself?
Try to change the type of the IMG1 property to ImageSource and set it like
IMG1 = new BitmapImage(new Uri("https://example.png"))
Then remove the BitmapImage from XAML and bind the Image control's Source property directly:
<Image ImageFailed="Image_ImageFailed" Source="{Binding IMG1}" />
Probably also force immediate query evaluation by calling ToList():
ItemsListBox.ItemsSource = data.Select(grp => grp.FirstOrDefault()).ToList();

Force an update to a control that's bound to the InkCanvas

I have the following XAML:
<Image Visibility="Visible"
Source="{Binding ElementName=inkCanvas,
Converter={StaticResource InkCanvasToImageSource},
UpdateSourceTrigger=PropertyChanged}">
</Image>
<InkCanvas x:Name="inkCanvas" />
What I'm trying to do is to convert the InkCanvas Stroke Collection into a BitmapImage. I'm using MVVM, and want to do this on a command. The problem that I have is that the above code will not trigger the converter to fire. I'm using UWP, so I can only pass one of the controls as a command parameter.
I need a method to convert from one to the other, but I'd like to do it inside the ViewModel.
An InkCanvas control is associated with an instance of an InkPresenter object (exposed through the InkPresenter property). The InkPresenter provides properties, methods, and events for managing the input, processing, and rendering of ink data for an InkCanvas control. So binding to InkCanvas won't trigger your converter to fire as the ink input is managed entirely by the InkPresenter. And InkCanvas.InkPresenter property is not a dependency property, we can't bind to this property. So we can't force the Image update with bingding to InkCanvas. We have to do it in code-behind and this may break the MVVM design.
To update the Image, we can use StrokesCollected and StrokesErased event to detect ink input and in these event save all InkStroke objects to a BitmapImage. For example:
In the XAML
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Image x:Name="MyImage" />
<Border Grid.Row="1" BorderBrush="Red" BorderThickness="2">
<InkCanvas x:Name="inkCanvas" />
</Border>
</Grid>
And in the code-behind:
public MainPage()
{
this.InitializeComponent();
...
inkCanvas.InkPresenter.StrokesCollected += InkPresenter_StrokesCollected;
inkCanvas.InkPresenter.StrokesErased += InkPresenter_StrokesErased;
}
private async void InkPresenter_StrokesErased(InkPresenter sender, InkStrokesErasedEventArgs args)
{
var image = await SaveAsync();
MyImage.Source = image;
}
private async void InkPresenter_StrokesCollected(InkPresenter sender, InkStrokesCollectedEventArgs args)
{
var image = await SaveAsync();
MyImage.Source = image;
}
private async Task<BitmapImage> SaveAsync()
{
var bitmap = new BitmapImage();
if (inkCanvas.InkPresenter.StrokeContainer.GetStrokes().Count > 0)
{
try
{
using (InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream())
{
await inkCanvas.InkPresenter.StrokeContainer.SaveAsync(stream);
stream.Seek(0);
bitmap.SetSource(stream);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
}
return bitmap;
}
Here I just set the image to MyImage, you can also set it to your ViewModel and use Binding in the Image.

How do I bind an Image dynamically in XAML?

The following displays an image correctly in a silverlight usercontrol:
Image image = pagingManager.BaseApp.DatasourceManager.GetImage("helloWorld1");
ContentArea.Content = image;
...
<ContentControl x:Name="ContentArea"/>
However, I want to dynamically bind the image to XAML, e.g. like this:
#region ViewModelProperty: MainImage
private Image _mainImage;
public Image MainImage
{
get
{
return _mainImage;
}
set
{
_mainImage = value;
OnPropertyChanged("MainImage");
}
}
#endregion
...
MainImage = pagingManager.BaseApp.DatasourceManager.GetImage("helloWorld1");
And my XAML is this but the result is that it shows nothing:
<Image Source="{Binding MainImage}"/>
What do I need to put in my XAML to make it display the image object I have in my ViewModelProperty?
The Image.Source property is of type ImageSource, so your ViewModel should expose an ImageSource. Image is a control, it has nothing to do in the ViewModel.
If you do expose an Image control in your ViewModel (which is definitely a bad idea), then you should display it in a ContentControl, not an Image control :
<ContentControl Content="{Binding MainImage}" />

Categories