WPF ListView not caching images - c#

I have a WPF application that uses a ListView with a grid that displays images directly from the web. When the list is populated the images load as expected, but as I scroll down (the list contains around 200 items on average) it starts reusing the items that aren't in view (as it should). However, this causes the images to be released from memory and as a result they get reloaded all over again when the user scrolls back up.
MainWindow.xaml
<ListView Grid.Row="3" ItemsSource="{Binding SearchResults}" Background="{StaticResource PrimaryBackground}" Foreground="{StaticResource PrimaryForeground}"
ui:GridViewSort.AutoSort="True" ui:GridViewSort.ShowSortGlyph="False" IsSynchronizedWithCurrentItem="True" VirtualizingStackPanel.IsVirtualizing="False">
<ListView.View>
<GridView>
<GridViewColumn Width="80">
<GridViewColumn.CellTemplate>
<DataTemplate DataType="{x:Type Foo}">
<Image>
<Image.Source>
<BitmapImage CacheOption="OnDemand" UriSource="{Binding PreviewImageUrl}" />
</Image.Source>
</Image>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Title" DisplayMemberBinding="{Binding Title}" ui:GridViewSort.PropertyName="Title" />
<GridViewColumn Header="Remix" DisplayMemberBinding="{Binding Remix}" ui:GridViewSort.PropertyName="Remix" />
<GridViewColumn Header="Artist" DisplayMemberBinding="{Binding Artist}" ui:GridViewSort.PropertyName="Artist" />
<GridViewColumn Header="Duration" DisplayMemberBinding="{Binding Duration}" ui:GridViewSort.PropertyName="Duration" />
<GridViewColumn Header="BPM" DisplayMemberBinding="{Binding Bpm}" ui:GridViewSort.PropertyName="Bpm" />
<GridViewColumn Header="Year" DisplayMemberBinding="{Binding Date}" ui:GridViewSort.PropertyName="Date" />
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="0,0,10,0" />
</Style>
</StackPanel.Resources>
<Button Command="{Binding ElementName=Window, Path=DataContext.Download}" CommandParameter="{Binding}">Download</Button>
<Button Command="{Binding ElementName=Window, Path=DataContext.CopyLink}" CommandParameter="{Binding}">Copy link</Button>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Setting the Image.Source property with a different CacheOption makes no difference. You can also see I disabled virtualization which is bad but it's the only way to have it keep the images in memory. Is there an easy way to stop this from happening while also enabling virtualization?

Add a readonly PreviewImage property to your search results item class that creates and holds the BitmapImage when it is first accessed:
public class SearchResult : INotifyPropertyChanged
{
private Uri previewImageUrl;
public Uri PreviewImageUrl
{
get { return previewImageUrl; }
set
{
previewImageUrl = value;
previewImage = null;
NotifyPropertyChanged(nameof(PreviewImageUrl));
NotifyPropertyChanged(nameof(PreviewImage));
}
}
private ImageSource previewImage;
public ImageSource PreviewImage
{
get
{
if (previewImage == null && previewImageUrl != null)
{
previewImage = new BitmapImage(previewImageUrl);
}
return previewImage;
}
}
...
}
and bind to it like this:
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding PreviewImage}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>

Try this:
<Image
HorizontalOptions="CenterAndExpand"
VerticalOptions ="CenterAndExpand">
<Image.Source>
<UriImageSource Uri="{Binding Image}"
CacheValidity="14"
CachingEnabled="true"/>
</Image.Source>
</Image>

Related

Implementing double-click event for ListView

I've seen plenty of examples of how to add a double-click event for each ListView item, but I can't figure out how to implement it to my code. The best example I found so far is this one in MS docs, but it still doesn't help me handle this :
<ListView Name="listViewItem" ClipToBounds="True" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" SizeChanged="ListView_SizeChanged" TextOptions.TextHintingMode="Animated" Margin="0,0,117,0">
<ListView.View>
<GridView AllowsColumnReorder="False">
<GridViewColumn Header="Task ID" Width="0">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskID}" TextWrapping="Wrap" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Tast Title" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskTitle}" TextWrapping="Wrap" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Task Deadline" Width="275">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskDeadline}" TextWrapping="Wrap" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Task Group" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskGroup}" TextWrapping="Wrap" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Task Contact" Width="200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskContact}" TextWrapping="Wrap" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Task Workers" Width="200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskWorkers}" TextWrapping="Wrap" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
I have a helpers class that sets values on load event ->
public void GetGridTasks(ref ListView listViewItem)
{
SqlCommand sqlCommandRefresh = new SqlCommand("", dataconnection);
sqlCommandRefresh.CommandText = "SELECT TaskID, TaskTitle, TaskDeadline, TaskGroup, TaskContact, TaskWorkers FROM Tasks";
SqlDataAdapter sqlAdapter = new SqlDataAdapter(sqlCommandRefresh);
DataTable dt = new DataTable("Tasks");
sqlAdapter.Fill(dt);
foreach (DataRow dr in dt.Rows)
{
dr["TaskWorkers"] = dr["TaskWorkers"].ToString().Replace("||", ", ");
}
listViewItem.ItemsSource = dt.DefaultView;
}
This is basically TextBlocks inside GridView inside ListView. Can't really wrap my head around fixing this mess.
Just use the MouseDoubleClick Event from the ListView.
Example:
<ListView MouseDoubleClick="ListView_MouseDoubleClick">
//Items here
</ListView>
In your code behind you simply add an event handler for that
void ListView_MouseDoubleClick(object sender, MouseButtonEventArgs e){
var item = ((FrameworkElement) e.OriginalSource).DataContext
var myItem = item as *CastToWhateverTypeYouNeed*
if (item != null){
//Here you have your item
}
}

Edit TextView column on ListView WPF

I'm Using WPF.
I have ListView with TextBox column and two checkboxs columns.
I want to edit the TextBox text by double click or something else.
What is the simple way to do that?
...
<GridViewColumn Header="Name" DisplayMemberBinding={Binding Path=fullName}" Width=500>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Name="txtName"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
...
Here is a sample way of doing this...
public partial class MainWindow : Window
{
public List<string> Items { get; set; }
public MainWindow()
{
InitializeComponent();
Items = new List<string>();
LoadItems();
DataContext = this;
}
private void txtName_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
TextBox currentTextBox = (TextBox)sender;
if (currentTextBox.IsReadOnly)
currentTextBox.IsReadOnly = false;
else
currentTextBox.IsReadOnly = true;
}
private void LoadItems()
{
Items.Add("Coffee");
Items.Add("Sugar");
Items.Add("Cream");
}
}
<Grid>
<ListView ItemsSource="{Binding Items}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Name="txtName" Text="{Binding Mode=OneTime}" IsReadOnly="True" MouseDoubleClick="txtName_MouseDoubleClick" Width="100"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
Here is an example I have from an application that I wrote. The column JobName was to be user editable. This example allows the column to be editable and also gets rid of the border and lets the background blend into the row so it doesn't appear to have a text box in it.
Those can be edited out (BorderThickness="0" Background="Transparent").
My example binds to an MVVM ViewModel property called JobName and is set to be "TwoWay" so that changes to the view model will also reflect on the UI.
<ListView x:Name="lvJobs" HorizontalAlignment="Left" Height="628" Margin="30,62,0,0" ItemsSource="{Binding Jobs}"
SelectedItem="{Binding SelectedJob, Mode=TwoWay}" VerticalAlignment="Top" Width="335">
<ListView.View>
<GridView>
<GridViewColumn Header="Active" Width="50">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsActive, Mode=TwoWay}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Job Name" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding JobName, Mode=TwoWay}" BorderThickness="0" Background="Transparent"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding User}" Header="User" Width="125"/>
</GridView>
</ListView.View>
</ListView>

WPF - Filling a ListView

I have a 2 column ListView and I'm trying to fill it using and IDictionary with the following code.
System.Collections.IDictionary entryList = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User);
foreach (System.Collections.DictionaryEntry de in entryList)
{
Row row = new Row();
row.Name1 = (string)de.Key;
row.Name2 = (string)de.Value;
this.list1.Items.Add(row);
}
public class Row
{
public string Name1 { get; set; }
public string Name2 { get; set; }
}
XAML:
<ListView x:Name="varList"
Grid.ColumnSpan="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Height="400"
Width="500"
Margin="0, 30, 0, 0">
<ListView.View>
<GridView>
<GridViewColumn Width="150" Header="Name" />
<GridViewColumn Width="350" Header="Path" />
</GridView>
</ListView.View>
</ListView>
But every row and column gets filled with "Project.Views.Row".
Anyone got any idea on how to fix it? Thank you very much.
A ListView (and every other control for that matter) will display the results of calling ToString when given an object to display.
For a standard class, thats its qualified name; Project.Views.Row in your example.
There are two ways to fix this:
Don't add an object. Instead, format the string as you want it ie:
list1.Items.Add(String.Format({0}:{1}, row.Name1, row.Name2));
Do this the right way and use MVVM. In this case your XAML needs a data template:
<ListView ItemsSource="{Binding Rows}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name1}"/>
<TextBlock Text="{Binding Name2}"/>
</StackPanel>
</DataTemplate>
<ListView.ItemTemplate>
</ListView>
For a grid view:
<ListView.View>
<GridView>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name1}" />
<GridViewColumn Header="Path" DisplayMemberBinding="{Binding Name2}"/>
</GridView>
</ListView.View>
Binding the ItemsSource is not actually necessary, but since we are doing everything the right way, you should do it so you are not directly manipulating the UI from code.

Updatinging an XAML GridView from C# in .NET 3.5

I have a simple (I think) app that reads an SQL database into variables in my program, and I then need to update a gridview defined in XAML with the data I read. I'm constrained to code for .NET 3.5. All the searching I've done on the books I have on XAML, MS .NET help and elsewhere on the Web shows countless examples of doing this from ASP.NET, but none from a C#-XAML combination. I've found doing simple things in XAML ++much++ more difficult and involved than doing the same thing in Winforms, and I'm just not getting this. In particular, data binding seems to me to be a black art. Can someone please look at my XAML and tell me what I need to do or change to populate this control from my C# code-behind?
Here is my XAML:
<Window x:Class="UCCResourceManager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="UCC Resource Mangler" Height="350" Width="700">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250"/>
<RowDefinition />
</Grid.RowDefinitions>
<ListView Name="grdResource"
ItemsSource="{Binding}"
Grid.Row="0">
<ListView.View>
<GridView AllowsColumnReorder="false"
ColumnHeaderToolTip="UCC Resource Table">
<GridViewColumn DisplayMemberBinding="{Binding Path=ID}"
Header="ID"
Width="50"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=LocationID}"
Header="LocationID"
Width="75"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Type}"
Header="Type"
Width="50"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Name}"
Header="Name"
Width="200"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Enabled}"
Header="Enabled"
Width="50"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Flags}"
Header="Flags"
Width="50"/>
</GridView>
</ListView.View>
</ListView>
<Button Name="btnOK"
Content="OK"
Grid.Row="1"
Width="100"
Height="20
" Click="btnOK_Click" />
</Grid>
</Window>
I recommand to use MVVM. There you have a ViewModel class where you can have your properties to bind to.
public class MainWindowViewModel
{
#region Constructor
public MainWindowViewModel()
{
YourGridList = new ObservableCollection<GridElement>();
var el = new GridElement
{
Element1 = "element 1",
Element2 = "element 2",
Element3 = "element 3"
};
YourGridList.Add(el);
}
#endregion
#region Private members
private ObservableCollection<GridElement> _yourGridList;
private ICommand _addElementCommand;
#endregion
#region Public properties
public ObservableCollection<GridElement> YourGridList
{
get
{
return _yourGridList;
}
set
{
_yourGridList = value;
}
}
#endregion
#region Commands
public ICommand AddElementCommand
{
get { return _addElementCommand ?? (_addElementCommand = new DelegateCommand(AddElement)); }
}
#endregion
#region Private Methods
private void AddElement()
{
var el = new GridElement
{
Element1 = "NewEl1",
Element2 = "NewEl2",
Element3 = "NewEl3"
};
YourGridList.Add(el);
}
#endregion
Then, you can use in the xaml.cs file this class as DataContext.
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
And from the xaml file you can have your ListView ItemsSource populated from the "YourGridList" property from the ViewModel.
<Window x:Class="WpfApplication6.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="35"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Content="Add element" Command="{Binding AddElementCommand}" Grid.Row="0"/>
<ListView Grid.Row="1"
Margin="10"
ItemsSource="{Binding YourGridList, UpdateSourceTrigger=PropertyChanged}"
MaxHeight="300">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Width="Auto">
<GridViewColumn.Header>
<GridViewColumnHeader Content="Element 1" HorizontalContentAlignment="Left" />
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Element1}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="Auto">
<GridViewColumn.Header>
<GridViewColumnHeader Content="Element 2" HorizontalContentAlignment="Left" />
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Element2}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="Auto" >
<GridViewColumn.Header>
<GridViewColumnHeader Content="Element 3" HorizontalContentAlignment="Left" />
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Element3}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Grid>
This should work for updating your ListView and have a clear decoupling between the code and the xaml part.

WPF ListView: Aligning text in selected columns

<ListView ItemsSource="{Binding MyData}">
<ListView.View>
<GridView>
<GridViewColumn Header="col1" DisplayMemberBinding="{Binding Path=value1}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock TextAlignment="Right" Text="{Binding Path=value1}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="col2">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock TextAlignment="Center" Text="{Binding Path=value2}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="col3" DisplayMemberBinding="{Binding Path=value3}"/>
</GridView>
</ListView.View>
<ListView ItemsSource="{Binding MyData}">
col1 is supposed to be right-aligned. (Not working)
col2 is supposed to be center-aligned. (Working)
col3 is supposed to be left-aligned. (Working)
Is there a reason DisplayMemberBinding is overriding CellTemplate? If so, is there a fix for this (while still using DisplayMemberBinding)?
Edit: I ended up implementing it like this:
<Window xmlns:util="clr-namespace:TestProject.Util">
<Window.Resources>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
<Style TargetType="GridViewColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<DataTemplate x:Key="value1Template">
<TextBlock TextAlignment="Right" Text="{Binding Path=value1}"/>
</DataTemplate>
<DataTemplate x:Key="value2Template">
<TextBlock TextAlignment="Right" Text="{Binding Path=value2}"/>
</DataTemplate>
</Window.Resources>
<Grid>
<ListView ItemsSource="{Binding MyData}" IsSynchronizedWithCurrentItem="True" util:GridViewSort.Command="{Binding SortCommand}">
<ListView.View>
<GridView>
<GridViewColumn Header="col1" CellTemplate="{StaticResource value1Template}" util:GridViewSort.PropertyName="value1"/>
<GridViewColumn Header="col2" CellTemplate="{StaticResource value2Template}" util:GridViewSort.PropertyName="value2"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
In the code behind:
private RelayCommand sortCommand;
public ICommand SortCommand { get { return sortCommand ?? (sortCommand = new RelayCommand(Sort)); } }
private void Sort(object param)
{
var propertyName = param as string;
var view = CollectionViewSource.GetDefaultView(MyData);
var direction = ListSortDirection.Ascending;
if (view.SortDescriptions.Count > 0)
{
var currentSort = view.SortDescriptions[0];
if (currentSort.PropertyName == propertyName)
direction = currentSort.Direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
view.SortDescriptions.Clear();
}
if (!string.IsNullOrEmpty(propertyName))
view.SortDescriptions.Add(new SortDescription(propertyName, direction));
}
DisplayMemberBinding has the highest priority. You can not use it combined with CellTemplate. See here in the remarks section.
If you want to right-or center-align the content, you must declare the CellTemplate with the binding (as you did) and remove the DisplayMemberBinding-attribute. If you also want to change the column header alignment, you must also set the GridViewColumn.Header-property.
Just add the following after your Window tag:
<Window.Resources>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Right" />
</Style>
</Window.Resources>

Categories