For a future professional project, I need to evaluate the WPF capabilities.
In this context, I created a small test project, which contains 1 string tree, and 1 image grid. I want that my image grid shows all the jpeg images contained inside a given directory, and for each image, to show the extracted file name below the image, without its path and extension.
Actually, my demo works correctly according to my goal, except for one point: I added each formatted file name to show inside a List collection, which I tried to bind with a TextBlock shown on the bottom of each images. However this formatted name isn't visible, instead I see the complete file name, as if the TextBlock extracted it directly from the Image object.
I tried to resolve this issue by myself, following several tutorials, nothing worked for me. I cannot figure out what I'm doing wrong. Can someone explain to me?
Here is my xaml file content
<Window x:Class="VirtualTrees.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:VirtualTrees"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style x:Key="myHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Visibility" Value="Collapsed" />
</Style>
<DataTemplate x:Key="itImageCell">
<WrapPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Image Width="120" Stretch="Uniform" Source="{Binding}"/>
<TextBlock Grid.Row="1" Width="120" Text="{Binding}" TextTrimming="CharacterEllipsis"/>
</Grid>
</WrapPanel>
</DataTemplate>
<local:ListToStringConverter x:Key="ListToStringConverter" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="400*"/>
</Grid.ColumnDefinitions>
<ListView Margin="10" Name="lvStringTree">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
<GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
</GridView>
</ListView.View>
</ListView>
<Grid x:Name="grImages" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<ListView Grid.Row="1" Name="lvImages" ItemsSource="{Binding Path=m_ImageList}" ItemTemplate="{StaticResource itImageCell}">
<ListView.Background>
<ImageBrush/>
</ListView.Background>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
<TextBlock Name="tbImageName" Text="{Binding Path=m_ImageNames, Converter={StaticResource ResourceKey=ListToStringConverter}}" DataContext="{StaticResource itImageCell}" />
</Grid>
</Grid>
</Window>
And my c# code
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace VirtualTrees
{
[ValueConversion(typeof(List<string>), typeof(string))]
public class ListToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(string))
throw new InvalidOperationException("The target must be a string");
return string.Join(", ", ((List<string>)value).ToArray());
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Mail { get; set; }
}
List<ImageSource> m_ImageList = new List<ImageSource>();
List<string> m_ImageNames = new List<string>();
string m_RegexPattern = @"\\([\w ]+).(?:jpg|png)$";
public MainWindow()
{
InitializeComponent();
PopulateStringTree();
PopulateImageGrid();
}
public void PopulateStringTree()
{
List<User> vstItems = new List<User>();
for (ulong i = 0; i < 100000; ++i)
{
vstItems.Add(new User() { Name = "John Doe", Age = 42, Mail = "[email protected]" });
vstItems.Add(new User() { Name = "Jane Doe", Age = 39, Mail = "[email protected]" });
vstItems.Add(new User() { Name = "Sammy Doe", Age = 7, Mail = "[email protected]" });
}
lvStringTree.ItemsSource = vstItems;
}
public void PopulateImageGrid()
{
// get jpeg image file list from target dir
string moviePosterPath = @"W:\Labo\WPF\VirtualTrees\VirtualTrees\Resources\Images";
List<string> fileNames = new List<string>(System.IO.Directory.EnumerateFiles(moviePosterPath, "*.jpg"));
// iterate through files
foreach (string fileName in fileNames)
{
// load image and add it to image list
m_ImageList.Add(new BitmapImage(new Uri(fileName)));
Console.WriteLine("filename " + fileName);
// extract image file name and add it to name list
Match regexMatch = Regex.Match(fileName.Trim(), m_RegexPattern);
m_ImageNames.Add(regexMatch.Groups[1].Value);
Console.WriteLine("Movie Name: " + regexMatch.Groups[1].Value);
}
// bind data to image grid
lvImages.ItemsSource = m_ImageList;
}
}
}
Your DataTemplate
is the origin of the error. You have to check the binding of the TextBlock
. You are binding to the DataContext
which is a BitmapSource
. The TextBlock
implicitly calls BitmapSource.ToString()
to get the string representation of the type. BitmapSource
has ToString()
overriden to return the full file path. To fix this, you need to use a IValueConverter
.
Modified DataTemplate
. The TextBlock
binding now uses a converter to convert the BitmapSource
to the filename:
<DataTemplate x:Key="itImageCell">
<WrapPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Image Width="120"
Stretch="Uniform"
Source="{Binding}" />
<TextBlock Grid.Row="1"
Width="120"
Text="{Binding ., Converter={StaticResource BitmapSourceToFilenameConverter}}"
TextTrimming="CharacterEllipsis" />
</Grid>
</WrapPanel>
</DataTemplate>
The IValueConverter for the TextBlock
binding to convert the BitmapSource
to filename:
[ValueConversion(typeof(BitmapSource), typeof(string))]
public class BitmapSourceToFilenameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is BitmapSource bitmapSource)
return bitmapSource.UriSource.AbsolutePath;
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
A small mistake I realized in your code:
You are first setting a binding on the ListView
:
<ListView Name="lvImages" ItemsSource="{Binding Path=m_ImageList}" />
and then you override (remove) it
// bind data to image grid
lvImages.ItemsSource = m_ImageList;
This is not a binding (the comment is not true).
You should make m_ImageList
a ObservableCollection<ImageSource>
instead of List
. The ObservableCollection
will automatically update the ListView
when items are added, moved or removed. Then remove this line from your MainWindow
class: lvImages.ItemsSource = m_ImageList;