WPF Data binding listview of images [duplicate] - c#

I am trying to display some images in a ListView and have not been successful. I am using WPF MVVM and the ListView is a holdover from simply displaying suits and rank data. (See my previous post: MVVM in WPF - How to alert ViewModel of changes in Model... or should I? if you are interested!) That is, I could use something other than ListView (if that is the advice) but I would still like to know how to do it with ListView, assuming it's doable. My property I'm binding to in the ViewModel is:
public ObservableCollection<Image> PlayerCardImages{
get{
ObservableCollection<Image> results = new ObservableCollection<Image>();
foreach (CardModel card in PlayerCards)
{
Image img = new Image();
BitmapImage bi3 = new BitmapImage();
bi3.BeginInit();
// TODO: Pick card based on suit/rank. Just get 1 image working now
bi3.UriSource = new Uri("diamond-1.png", UriKind.Relative);
bi3.EndInit();
img.Stretch = Stretch.Fill;
img.Source = bi3;
results.Add(img);
}
return results;
}
}
In my XAML code I'm using:
<Window.Resources>
<DataTemplate x:Key="ImageCell">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding PlayerCardImages}" Width="200" Height="200" Stretch="Fill" ToolTip="Add tooltip"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel Orientation="Vertical">
<Label Content="Player Cards"/>
<ListView Name="lvwTitles" ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True"
SelectionMode="Single" ItemTemplate="{StaticResource ImageCell}" Height="59">
</ListView>
</StackPanel>
This idea was shamelessly stolen from: WPF - bind images horizontally to ListView However, it doesn't even appear to databind as evidenced by my breakpoint in PlayerCardImages not being hit.
I also tried the following XAML with somewhat better luck:
<StackPanel Orientation="Vertical">
<Label Content="Player Cards"/>
<ListView
AlternationCount="2"
DataContext="{StaticResource PlayerCardsGroups }"
ItemsSource="{Binding}"
>
<ListView.GroupStyle>
<StaticResourceExtension
ResourceKey="CardGroupStyle"
/>
</ListView.GroupStyle>
<ListView.View>
<GridView>
<GridViewColumn>
<Image Height="50" Width="40"></Image>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
<Window.Resources>
<CollectionViewSource x:Key="PlayerCardsGroups"
Source="{Binding Path=PlayerCardImages}">
</CollectionViewSource>
<GroupStyle x:Key="CardGroupStyle">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock
x:Name="txt"
Background="{StaticResource Brush_HeaderBackground}"
FontWeight="Bold"
Foreground="White"
Margin="1"
Padding="4,2,0,2"
Text="Cards"
/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
<Style x:Key="CardItemStyle" TargetType="{x:Type ListViewItem}">
<!--
Stretch the content of each cell so that we can
right-align text in the Total Sales column.
-->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!--
Bind the IsSelected property of a ListViewItem to the
IsSelected property of a CustomerViewModel object.
-->
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ItemsControl.AlternationIndex" Value="1" />
<Condition Property="IsSelected" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#EEEEEEEE" />
</MultiTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
this code definitely goes through databinding - my breakpoint is hit at the beginning of the program and whenever items are added to the collection. But no images is displayed. Rather than torture you with more XAML that does not work, perhaps I could ask someone to point me to some code/examples/docs that show how to bind a list of Images to a ListView (or another control if you really feel that ListView is inappropriate). Notice that my collection is the stuff I'm binding to. I notice that with many examples, they are binding to a subproperty. I.e. they may have a collection of albums and for each album they bind to it's property image (see: Showing items as images in a WPF ListView).
Any ideas or help would be much appreciated.
-Dave
Additional info.
Based on suggestions by Clemens, I now have this code for PlayerCardImages:
public ObservableCollection<ImageSource> PlayerCardImages
{
get
{
var results = new ObservableCollection<ImageSource>();
//if (PlayerCards.Count == 0)
// return results;
//else
//{
// results.Add(new BitmapImage(new Uri(#"Images\\" + "diamond-1.png", UriKind.Relative)));
//}
foreach (var card in PlayerCards)
{
results.Add(new BitmapImage(new Uri(#"Images\\" + GetCardFileName(card), UriKind.Relative)));
}
return results;
}
I used the exact XAML he suggested. It almost works. I say "almost" because I noticed strange behavior whereby sometimes 1 card would show and sometimes not (I never got 2 cards). All the getting cards from files and binding seems to be working and I tracked down what I think is key to the last remaining bug (and it's BIZARRE). If in the debugger, I examine results, and further open up results[0] in the debugger, I get that card displayed! I actually have to open up [0] (you see info about height, width, etc.) for this to work. Furthermore if I open up [1], I get that card displayed instead. Why would opening up the debugger info have any effect? For those of you who might ask, what happens if you open up both cards in the debugger... that doesn't work. I get a operation timed out exception. I will say that perhaps my image files are big. 10Kbytes to 30 Kbytes. Is that the problem? I'm guessing not, and that it's a subtle problem with reading in the images or binding. What is going on? Thanks, Dave

First, you should not use Image controls in your ViewModel. You already have an Image control in the DateTemplate of your view. You want to bind the Source property of this Image conntrol, and the source property of this binding can't be another Image.
Instead your ViewModel would either use ImageSource (or a derived class like BitmapImage) as image type, like this:
public ObservableCollection<ImageSource> PlayerCardImages
{
get
{
var results = new ObservableCollection<ImageSource>();
foreach (var card in PlayerCards)
{
results.Add(new BitmapImage(new Uri(card.ImageUrl)));
}
return results;
}
}
Or simply the image URIs or paths, as there is an automatic conversion from string to ImageSource built into WPF:
public ObservableCollection<string> PlayerCardImages
{
get
{
var results = new ObservableCollection<string>();
foreach (var card in PlayerCards)
{
results.Add(card.ImageUrl);
}
return results;
}
}
You would now bind your Listbox's ItemsSource property to the PlayerCardGames collection, and in the DataTemplate you bind directly to the collection item. The ListView's DataContext has to be set to an instance of the object that defines the PlayerCardGames property.
<ListView ItemsSource="{Binding PlayerCardGames}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
UPDATE: As there seems to be a problem with loading the image files, you may try the following method. It loads images synchronously and you are able to step through with the debugger.
public static ImageSource LoadImage(string fileName)
{
var image = new BitmapImage();
using (var stream = new FileStream(fileName, FileMode.Open))
{
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
}
return image;
}
You can use this method in your PlayerCardGames property getter like this:
foreach (var card in PlayerCards)
{
results.Add(LoadImage(#"Images\\" + GetCardFileName(card)));
}

I didn't really try to reproduce your problem, but I will if this is not solving it:
In your first xaml block, I think you mixed up the bindings. This would be how I expect it, ItemsSource to the ObservableCollection of Images, Image source to the Image.
<Image Source="{Binding}" ... />
<ListView Name="lvwTitles" ItemsSource="{Binding PlayerCardImages}" ... />
In your second block, you omitted the Source binding altogether:
<Image Source="{Binding}" Height="50" Width="40" />

Related

What is the best approach for positioning and overlaying images in WPF?

I have a map as a bitmap, on top of which I add markers - which can have different icons of different sizes, which contain data, and have various other options. The map is an image, and the way it's implemented now, is redrawing the map's area at certain position, with marker icons by iterating over the picture and setting pixels one by one using marker's image. But I realize this is probably not the best option avaiable so I am asking here.
I was thinking about using markers as controls, but I am not sure how I would accurately and programatically get and set the markers positions.
A usual pattern followed in WPF apps is MVVM - you should have a look at online resources to get familiar with it and how it can leverage the power of WPF.
To answer your specific problem, you would build your app in a way that is data-driven: you create and expose a collection of markers as data objects and you bind the view to this collection by telling how an individual marker should be displayed.
Let's create a MarkerViewModel class that will contain all information the view needs to display them:
public class MarkerViewModel {
public double X { get; set; }
public double Y { get; set; }
public string Description { get; set; }
}
Your MainWindow code-behind could be:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
DataContext = new ObservableCollection<MarkerViewModel> {
new MarkerViewModel() { X = 30, Y = 30},
new MarkerViewModel() { X = 100, Y = 20},
new MarkerViewModel() { X = 100, Y = 150}
};
}
}
We put some data in a collection (of type ObservableCollection) and assigned it to the DataContext property of the MainWindow.
Its XAML could be:
<Grid>
<Image Source="Chrysanthemum.jpg" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<ItemsControl ItemsSource="{Binding}" HorizontalAlignment="Left" VerticalAlignment="Top">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:MarkerViewModel}">
<StackPanel>
<Ellipse Width="10"
Height="10"
Fill="Blue"/>
<TextBlock FontWeight="Bold" Text="{Binding Description}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
The Grid is here to overlay its two children: an Image which is the background picture and could be your base map, and an ItemsControl whish is repsonsible for displaying a view for each item in the DataContext collection.
The ItemsControl.ItemTemplate property lets you tell WPF how you want each marker displayed: I chose here a blue Ellipse and a TextBlock.
The ItemsControl.ItemContainerStyle lets you tell WPF where you want to position each item it creates (it can do much more).
I suggest you start reading some tutorials on data binding, styling and templating and ItemsControl specifically. Also read more on the MVVM pattern, but hopefully my example will help you get kickstarted.

How do I Access Items of a Listbox

I have a Listbox done like this
<ListBox x:Name="lbAlbumSelect">
<ListBox.ItemTemplate>
<DataTemplate>
<Button>
<Button.Content>
<StackPanel>
<Image />
<TextBlock TextWrapping="Wrap"
Text="{Binding album_name}" />
</StackPanel>
</Button.Content>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I want to access every Image programmatically and set its Source. I tried to navigate the listbox like this
foreach (Button btn in lbAlbumSelect.Items)
{
StackPanel stack=btn.Content;
Image image=stack.Children.ElementAt(0) as Image;
//every ListBoxItem is binded to a clsAlbums object that contains various data,
//also the name of the image file, but not the path.
string pathImg = #"/Assets/Images/" + (btn.DataContext as clsAlbums).album_img;
LoadImage(pathImg, image); //function that sets image source to path img
}
But gives me a Invalid Cast Exception on the foreach clause.
Is there a faster and more correct way to do this?
You should ideally be binding the image source to the control. Add an additional property to your class clsAlbums which can be bound to the Image source.
public class clsAlbum
{
public string album_name { get; set; }
public string album_img { get; set; }
public string album_img_src
{
get
{
return #"/Assets/Images/" + album_img;
}
}
}
Now bind this additional property album_img_src to your xaml.
<ListBox x:Name="lbAlbumSelect">
<ListBox.ItemTemplate>
<DataTemplate>
<Button>
<Button.Content>
<StackPanel>
<Image Source="{Binding album_img_src}"/>
<TextBlock TextWrapping="Wrap"
Text="{Binding album_name}" />
</StackPanel>
</Button.Content>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The item is actually ListBoxItem. In your case Button is content of ListBoxItem and Content of Button is StackPanel and Image is child of StackPanel. So you need to traverse visual tree somehow and you can do so using Linq to visual tree, for example. http://www.codeproject.com/Articles/63157/LINQ-to-Visual-Tree
Probably easiest way of accessing elements inside datatemplate is from it's loaded or initialized event:
here:
<Image Loaded="Image_Loaded" />
void Image_Loaded(object sender, EventArgs e){
var image =(Image)sender;
Try to avoid acessing elements inside datatemplates. 90% times you achieve your goal better, using ViewModel, converters, using behaviours, datatriggers or extracting datatemplate to separate UserControl

Duplicating the button and adding it to dockpanel in WPF/C#

I am trying to add the button to my DockPanel dynamically. I need to create the same button which exist in my dockpanel.
<Button Name="ImageMoreButton"
DockPanel.Dock="Right"
Command="{Binding LaunchLookup}"
Style="{StaticResource ButtonStyle}"
Margin="2,0,2,0"
Padding="3"
Visibility="{Binding Definition.IsLookupImageButton, Converter={StaticResource boolToVisibilityConverter}}"
IsEnabled="{Binding Locked, Converter={StaticResource invertedBooleanConverter}}">
<Image Name="button_image" Source="search_button_rest.png"/>
</Button>
Here is my C# code.
d.Name = VariableArg.Name + index;
d.Margin = VariableArg.Margin;
item.Command = ImageMoreButton.Command;
item.Style = ImageMoreButton.Style;
item.Name = ImageMoreButton.Name + index;
item.Visibility = ImageMoreButton.Visibility;
item.Padding = ImageMoreButton.Padding;
item.Margin = ImageMoreButton.Margin;
item.IsEnabled = ImageMoreButton.IsEnabled;
item.Height = ImageMoreButton.ActualHeight;
item.Width = ImageMoreButton.ActualWidth;
DockPanel.SetDock(item, Dock.Right);
Let me know if this is the correct way to that.
WPF Controls cannot be added to two different parent controls. If you wish to add a copy of an item at runtime, you need to create a new object entirely, not re-use an existing item.
That said, since your buttons represent a Configuration setting, I would recommend you use something like an ItemsControl that is bound to a collection of data objects, with the Button being used as the ItemTemplate.
For example, suppose you had an ObservableCollection<MySetting> collection called Settings. You could then write the following XAML:
<ItemsControl ItemsSource="{Binding Settings}">
<!-- ItemsPanelTemplate -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- ItemContainerStyle -->
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="DockPanel.Dock" Value="Right" />
</Style>
</ItemsControl.ItemContainerStyle>
<!-- ItemTemplate -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.LaunchLookup, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
Style="{StaticResource ButtonStyle}"
Margin="2,0,2,0"
Padding="3"
Visibility="{Binding Definition.IsLookupImageButton, Converter={StaticResource boolToVisibilityConverter}}"
IsEnabled="{Binding Locked, Converter={StaticResource invertedBooleanConverter}}">
<Image Source="search_button_rest.png"/>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Then to add new items, you would simply add items to the ObservableCollection
Settings.Add(new MySetting());
For other examples using an ItemsControl, check out this post I wrote
If you use this code a lot I suggest you define an extension method on Button class. like this :
public static class ButtonExtension
{
public static Button Clone(this Button myButton, int index)
{
var newButton = new Button
{
Command = myButton.Command,
Style = myButton.Style,
Name = myButton.Name + index,
Visibility = myButton.Visibility,
Padding = myButton.Padding,
Margin = myButton.Margin,
IsEnabled = myButton.IsEnabled,
Height = myButton.ActualHeight,
Width = myButton.ActualWidth
};
return newButton;
}
}
which you can use later like :
var newButton = ImageMoreButton.Clone(index);
DockPanel.SetDock(newButton, Dock.Right);
Talking about "a better way", I recommend you to get faimliar with the MVVM Pattern since it's very powerful in simplifying the UI code. Also, it seems to be a widespread best practice for WPF programming since WPF has great infrastructure for it. There certainly is a learning curve, but once you understand it, you'll solve such puzzles easily. I could provide a sample, but I'm not sure that it would be useful here.
If you need a solution right now, you can use your code, it seems to be fine. But don't forget to add new controls to the parents:
parentPanel.Children.Add(item);

Creating custom column header in DataGrid - binding problem

I'm trying to create a custom header for some of the columns in my generic DataGrid; I want these headers to include a text box which I can use to apply fiters to the data.
This is what I have so far:
<Style x:Key="ColumnHeaderStyle" TargetType="dataprimitives:DataGridColumnHeader">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding}"/>
<TextBox Width="Auto" Tag="{Binding Path=Tag}" LostFocus="TextBox_LostFocus" TextChanged="TextBox_TextChanged" MinWidth="50"/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
This is the style used by the header I'm toying with at the moment. Here's the code generation which feeds the header the appropriate data on creation:
private void dataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
(...)
e.Column.Header = new { Text = e.Column.Header, Tag = e.PropertyName };
e.Column.HeaderStyle = Resources["ColumnHeaderStyle"] as Style;
(...)
}
When the application is ran, the TextBlock of a column will contains the following: { Text = Description, Tag = Description }
As such, I'd expect the Path=Tag part of the binding to work, however, when the TextChanged event is hit, the tag is null.
What am I doing wrong?
Apparently using anonymous types doesn't work well with XAML and binding... which is odd, as it uses reflection, as far as I know.
Creating a simple public class for storing the data and using it instead of the anonymous type solved the problem.

How to find a data-binded ListBoxItem position?

I have a ListBox that its ItemsSource is given from a class based on the data binded items template. I want to find ListBox.SelectedItem position relative to the ListBox. Since I've used a class to feed ItemsSource, I'm not be able to cast ListBox.SelectedItem (which has a type of object) to the ListBoxItem. (Instead I should cast it to the source class type.)
What's the way? -Thanks
Details: (Arbitrary)
There is a ListBox which implements a Style like so:
<Style x:Key="MyListBoxStyle" TargetType="{x:Type ListBox}">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Border ...>
<StackPanel ...>
<Image Source="{Binding Path=ItemImageSource}" .../>
<TextBlock Text="{Binding Path=ItemTitle}" .../>
</StackPanel>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
The ListBox has been used as follows:
<ListBox x:Name="MyListBox"
ItemsSource="{Binding}"
Style="{StaticResource ResourceKey=MyListBoxStyle}"/>
Also there is a class that supports MyListBox data-binding info:
internal class MyListBoxItemBinding
{
public string ItemTitle { get; set; }
public ImageSource ItemImageSource { get; set; }
}
And to feed the MyListBox:
MyListBox.ItemsSource = new List<MyListBoxItemBinding> { /* some items */ };
Now, how can I find MyListBox.SelectedItem location relative to the MyListBox?
Use ItemsControl.ItemContainerGenerator to get a reference to the item container generator for your ListBox (this is the object that creates wrappers for all your databound objects).
Then, use the ItemContainerGenerator.ContainerFromItem method to get a reference to the UIElement that represents the selected ListBoxItem.
Finally, see the answer to this question to for a way of getting the coordinates of the selected item relative to the ListBox.

Categories