Reordering ListView items in UWP messes up the content - c#

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

Related

ImageFailed with GridView

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

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();

Out of Memory Exception when Populating ListView with Images (Windows Phone 8.1)

So I am able to display images from my custom folder in the Pictures Library to my ListView in the app. However, when that custom folder has 3 or more images, it's either Out of Memory Exception occurs or the app just crashes and Visual Studio doesn't even realize that the app have crashed. My question is how can I make this work?
Here are my codes...
In the xaml.cs file:
List<StorageFile> FileList = (await temp.GetFilesAsync()).ToList();
List<ImageItem> ImageList = new List<ImageItem>();
for (int i = 0; i < FileList.Count; i++)
{
using (IRandomAccessStream FileStream = await FileList[i].OpenAsync(FileAccessMode.Read))
{
using(StorageItemThumbnail thumbnail = await file.GetThumbnailAsync(ThumbnailMode.PicturesView))
{
if (thumbnail != null && thumbnail.Type == ThumbnailType.Image)
{
BitmapImage bitmap = new BitmapImage();
await bitmap.SetSourceAsync(FileStream);
ImageList.Add(new ImageItem() { ImageData = bitmap });
}
}
}
}
this.PhotoListView.DataContext = ImageList;
Here is my Helper Class:
public class ImageItem
{
public BitmapImage ImageData { get; set; }
}
Here is my xaml ListView code:
<ListView Grid.Column="1"
Grid.Row="0"
x:Name="PhotoListView"
Grid.RowSpan="1"
ItemsSource="{Binding}">
<ListView.ItemTemplate>
<DataTemplate>
<Image Source="{Binding ImageData}"
Margin="10"/>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
The problem with your code is that when you use BitmapImage you didn't specify the DecodePixelHeight and DecodePixelWidth, you can solve the issue in 2 ways:
the first is to specify the DecodePixelHeight and DecodePixelWidth, the second is to pass the path of the image to the list view using this code:
List<StorageFile> FileList = (await temp.GetFilesAsync()).ToList();
List<string> ImageList = new List<string>();
foreach(var file in FileList)
{
ImageList.Add(file.Path);
}
this.PhotoListView.DataContext = ImageList;
the Image control is able to do all the stuff for you, and takes care of the memory management as well.
I think your main problem is settings the ItemsPanelTemplate to Stackpanel. This kills virtualization. There is no reason for you to override the default item panel.
Also as frenk91 mentions, adding DecodePixelHeight and DecodePixelWidth to your XAML may be useful.

List box with over 100 items crashes

I am working on windows phone 8 app.
I have List box with over 200 items to display.
<DataTemplate x:Key="DataTemplate1">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="White" Height="400" Width="400" CornerRadius="30,30,30,30">
</Border>
<Grid Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Top">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="5,20,5,5"
Foreground="#000000"
Text="{Binding Title}"/>
</Grid>
</Grid>
</DataTemplate>
But it crashes, i have debugged it till 100 items it works but after that it crashes.
In the PhoneApplicationPage_Loaded method i have
private void PhoneApplicationPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
myList.Add(new MyObject("A","A value"));
myList.Add(new MyObject("B", "B value"));
myList.Add(new MyObject("C", "C value"));
and so on... 200 items
ListBoxItems.ItemsSource = myList;
}
how can i fix this ?
Update :
<ItemsPanelTemplate x:Key="ItemsPanelTemplate">
<local:CollectionFlowPanel ItemHeight="400"
ItemWidth="400"
FocusedItemOffset="120"
UnfocusedItemOffset="20"
ItemVisibility="5">
<VirtualizingStackPanel />
</local:CollectionFlowPanel>
</ItemsPanelTemplate>
</phone:PhoneApplicationPage.Resources>
<Grid x:Name="LayoutRoot" Background="#000000">
<local:CollectionFlow x:Name="ListBoxItems"
ItemTemplate="{StaticResource DataTemplate}"
ItemsPanel="{StaticResource ItemsPanelTemplate}"/>
</Grid>
Ensure you have VirtualizingStackPanel inside the ItemsPanelTemplate of your list box, see this answer for more info.
Here's the XAML you likely need for your ListBox:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
You need to read following blog from msdn on visualization of the data in list and grid.
Using virtualization with a list or grid
Without seeing your whole xaml code I cannot suggest the exact answer but my guess is that you in xaml ListBox is placed inside a canvas/StackPanel or scrollviewer control.
When the size of the ItemsControl's viewport isn't restricted, the control doesn't perform virtualization. Instead, it creates an item container for each item in its collection. Some common containers that don't restrict the viewport size are Canvas, StackPanel, and ScrollViewer. You can enable virtualization in this situation by setting the size of ItemsControl directly, instead of letting it be sized by its parent container.
Here, we set the Height and Width on the GridView. This restricts the size of the viewport, and items outside of the viewport are virtualized.
Below are 2 scenarios one will throw out of memory exception and other will work fine(use your same code behind and test)
1. ListBox in Canvas
<Canvas .....
<ListBox Name="ListBoxItems".....
</ListBox>
</Canvas>
Above code will throw out of memory exception as items control's viewport is not defined (if you still want to use Canvas than define width/height if ListBox in that case the port of Items control is defined and virtulazation will apply)
2. ListBox in Grid
<Grid .....
<ListBox Name="ListBoxItems".....
</ListBox>
</Grid>
The above code will not throw out of memory exception as virtuallization is applied on the listbox.
Hope this will help
How big is your object ? If your object is too big you might not be able to load them all at once.
Did you try using the for loop?
public List<Fellow> fellowList { get; set; }
private void PhoneApplicationPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
fellowList = new List<Fellow>();
for (int i = 0; i < 2; i++)
{
Fellow fellow = new Fellow();
fellow.x = "B" + i;
fellow.value = "B Value" + i;
fellowList.Add(fellow);
}
this.DataContext = this;
ListBoxItems.ItemsSource = fellowList;
}
public class Fellow
{
public string x { get; set; }
public string value { get; set; }
}
Hope it helps..change the view model according to your wish

Clear image cache from Grid background

Hopefully I can explain what I need and what the problem is
I have the followings list box
<ListBox Margin="0,8,0,0" toolkit:TiltEffect.IsTiltEnabled="True" x:Name="ImageList" ItemsSource="{Binding Images}" HorizontalAlignment="Center" BorderThickness="4">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="{Binding imageID, Converter={StaticResource ImageConverter}}" Width="125" Height="125" Margin="6" Tap="list_OnTap">
<TextBlock Name="description" Foreground="{Binding TextColor}" Text="{Binding text}" Visibility="{Binding ShowText, Converter={StaticResource BooleanToVisibilityConverter}}" VerticalAlignment="Center" HorizontalAlignment="Center" TextAlignment="Center" TextWrapping="Wrap"/>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu IsZoomEnabled="False" Name="deletectx">
<toolkit:MenuItem Header="delete" Click="delete_Click"/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
The above code makes a grid of images, I used Grid background to show the image because I needed to show my text on the image (Not sure if there is any other way to do this)
This page load about 30 images with the size 125x125 pixel and about 4kb each. I noticed that it consumes a lot of memory. I read some posts here about clearing the cache of the images, but I don't know how I'm supposed to do that with the above code considering I'm setting a grid background as my image not an image control.
I might be able to access the grid inside the listbox, but whatever I do with it, will be applied to the first image only not the rest. I need to clear the images cache on my navigate away event.
Another question , I also have some performance problem , entering this page takes a little bit of time and I get low frame rate warning in Windows Phone App Analyser, not sure if what I'm doing (loading images through Converter for each listbox item) is right or not !
How can I make this faster ?
To be sure that you can reclaim any memory used by the default image caching behaviour you can use to following: (snipped from another project and edited slightly but should work as is.)
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
if (e.NavigationMode == NavigationMode.Back)
{
// Unload all images so as to reclaim any allocated memory
foreach (var child in VisualTreeHelperEx.GetVisualChildren(this.ImageList).Where(c => c is Image))
{
var image = child as Image;
if (image != null)
{
var bitmapImage = image.Source as BitmapImage;
if (bitmapImage != null)
{
System.Diagnostics.Debug.WriteLine("unloading " + bitmapImage.UriSource);
bitmapImage.UriSource = null;
}
image.Source = null;
}
}
}
}
Which uses this helper:
public static class VisualTreeHelperEx
{
public static IEnumerable<DependencyObject> GetVisualChildren(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return GetVisualChildrenAndSelfIterator(element).Skip(1);
}
public static IEnumerable<DependencyObject> GetVisualChildrenAndSelfIterator(DependencyObject element)
{
Debug.Assert(element != null, "element should not be null!");
yield return element;
int count = VisualTreeHelper.GetChildrenCount(element);
for (int i = 0; i < count; i++)
{
yield return VisualTreeHelper.GetChild(element, i);
}
}
}
This may have it's own performance issues so use with care and test the impact on actual performance.
The performance problem on page load is probably due to all the images being loaded at once. You could drip feed them into the list after the page has loaded to avoid this. If the actual images are larger than the background area you should set the DecodePixelHeight and DecodePixelWidth. It may also be worth removing the converter and adding a property with the full path as lots of complex converters can impact performance.

Categories