WPF: ListView with Images from a folder - wpf

I'm sort of new to WPF, but I have to do this and it's taking a lot of my time. I've searched for a solution but there are many alternative solutions and I honestly don't understand most of this. I have this XAML code:
<ListView Name="Thumbnails">
<ListView.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Height="30" Width="30" Margin="5"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
As well as this codebehind:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
DirectoryInfo folder = new DirectoryInfo(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + #"\SlikeSportista\");
FileInfo[] images = folder.GetFiles("*.jpg");
foreach (FileInfo img in images)
{
Thumbnails.Items.Add(img);
}
}
I've also tried this line of code in the foreach loop:
Thumbnails.Items.Add(System.Drawing.Image.FromFile(img.FullName));
In both cases the items are added, but the images are not displayed correctly, or rather, at all. You can select them, and there are the same amount of elements as there are in the folder, but there is no display.
Another question (less important one) would be how to display the images in squares instead of rows. Basically I want to have about 4 or so images per row, but now I have only 1 element per row, stretched all the way (although I can't see what is being displayed).

In your first attempt, you're adding FileInfo objects to the ListView's items collections. These aren't automatically converted to ImageSource items, as required by the binding in your DataTemplate. Add the FileInfo's FullName instead:
foreach (FileInfo img in images)
{
Thumbnails.Items.Add(img.FullName);
}
In your second attempt the problem is that you add instances of System.Drawing.Image, which is not part of WPF, but WinForms, and will also not be converted automatically. You may use BitmapImage instead:
foreach (FileInfo img in images)
{
Thumbnails.Items.Add(new BitmapImage(new Uri(img.FullName)));
}
The difference between both solutions is that in the second one you manually create image objects, whereas the first one relies on automatic conversion from string to ImageSource, which is built into WPF as a TypeConverter.
A solution for your second question would be to replace the ListView's ItemsPanel, perhaps by a UniformGrid:
<ListView Name="Thumbnails">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
...
</ListView>

As a general rule, you should keep in mind that adding images in the code-behind file (.xaml.cs file) is bad practice. In WPF there is a very widely used and common design pattern called MVVM (Model-View-ViewModel) which you should familiarize with and use. In your case, you should have had a ViewModel class containing an IEnumerable<BitmapImage> property that contains the images you wish to display in your ListView.
For example, let's say your ViewModel class is called ImagesViewModel and your view is ImagesView:
ImagesViewModel will have a property called:
ObservableCollection<BitmapImage> Images
ImagesView will contain:
<ListView Name="Thumbnails" ItemsSource="{Binding Images}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
...
Now, if you add / remove images to Images, they will be automatically added / removed from your list view (you have to implement the INotifyPropertyChanged interface in your view model and you're done).

Related

Variable Binding-Path

I need to display a memory dump for a technical application. Each Byte (Cell) should be defined via a DataTemplate to show additional information (highlight via setting Background color, individual Tooltip etc). I made the following attempt:
<DataTemplate x:Key="HexNumberTemplate">
<Grid>
[...]
<TextBlock>
<TextBlock.Text>
<Binding Path="Cell[0].Value">
<Binding.Converter>
[...]
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
The final result should look like this:
My problem is the fix coded Binding path. 'Cell' is a list of objects that holds all necessary information to display the cell. Using this approach, I need to define 16 times the same DataTemplate with Cell[0] to Cell[15]. I definitely want to avoid this!
I read an approach defining the DataTemplate in source code where I assemble the XAML in a string and call Markup.XamlReader.Load(MemoryStreamOfTheString). But here I lose the comfort of the Visual Studio IDE.
Is it possible to define the DataTemplate in XAML and make the indexer of the Cell-Object a parameter?
You should do like you have read: create templates dynamically, by loading them with XamlReader. In order to have comfort of XAML editor, you can define your template in a separate xaml file like this:
<DataTemplate
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid DataContext="{Binding Current_Cell}">
<!--Your template controls goes here.-->
</Grid>
</DataTemplate>
Then set type of this file to Resource, and load it into string and simply replace Current_Cell with each individual cell numbers before you load template from string when you construct your view.
By setting DataContext of Grid you help yourself to use other binding inside the template (context is already set to the current cell, and you don't need to replace it everywhere).
I was in a the same situation recently, only difference was, that my grid had totally dynamic columns (loaded from server), so I didn't even have the opportunity to create 16 templates :)
Try it with ListBoxes.
The outer ListBox includes the rows which are ListBoxes too, each of them binded to a List object. And you can create the DataTemplate of the ListBoxItems.
<DataTemplate x:Key="innerListBoxItem">
[...]
<TextBlock Text="{Binding Value}" />
[...]
<DataTemplate>
<DataTemplate x:key="outerListBoxItem">
<Grid>
<ListBox ItemTemplate="{StaticResource innerListBoxItem}" ItemCollection="{Binding Cells}"/>
</Grid>
<DataTemplate>
and whereever you want to put this control:
<ListBox ItemTemplate="{StaticResource outerListBoxItem}" ItemCollection={Binding CellsList}"/>
code behind:
public class ListOfCells { public List<Cell> Cells {get; set; } }
public List<ListOfCells> CellsList {get; private set; }
You can try and use the Attached Behavior pattern. You can bind an attached property to the column number, and the attached behavior will bind the text to the required cell given the column number.
I would suggest to use a single column DataGrid with custom Header and Cell templates.
You grid won't benefit from indivudual cells resizing, will it? Your header is going to have a fixed number of columns, you cell template can be implemented as a subclass of ListControl - we just need to change StackPanel's orientation from vertical to horizontal. Then, your bound object will be a collection of bytes, which is easy as your cell control is derived from ListControl.
Please let us know if that makes sense.

Display a running number in the WPF DataGrid RowHeader

I am using WPF with MVVM.
How can I bind:
a list of numbers
or a list of clr objects with property number
or a list of strings
in my ViewModel to the RowHeader of the WPF DataGrid ?
I found an elegant solution for this problem from http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/7d0cbdf2-dea3-4dd4-a570-08aa6c78d911. Here is recap:
<tk:DataGrid x:Name="dg" LoadingRow="dg_LoadingRow" ItemsSource="{Binding}">
<tk:DataGrid.RowHeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type tk:DataGridRow}}, Path=Header}"></TextBlock>
</DataTemplate>
</tk:DataGrid.RowHeaderTemplate>
</tk:DataGrid>
private void dg_LoadingRow(object sender, Microsoft.Windows.Controls.DataGridRowEventArgs e)
{
e.Row.Header = (e.Row.GetIndex() + 1).ToString();
}
However, I couldn't make the number on the row header right aligned. Adding HorizontalAlignment="Right" to the TextBlock doesn't help. I also tried to wrap the TextBlock inside a StackPanel, and set this property, but it doesn't work either. Any idea?
You could take this approach http://www.codeproject.com/KB/WPF/MVVM_DataGrid.aspx?msg=3241301
Basically you are creating an list that will handle the numbering for you, as long as the object of T implements ISequenceObject.
If you wanted to go the other direction you could look at handling the LoadingRow Event and simply adding 1 to a known DataColumn. This would not break the concept of MVVM as a numbered column is not part of the business logic, but part of the presentation.
To make the Numbered DataGrid reusable you could also inherit from the DataGrid and create you own Numbered DataGrid.

WPF ListBox with a ListBox - UI Virtualization and Scrolling

My prototype displays "documents" that contain "pages" that are
represented by thumbnail images. Each document can have
any number of pages. For example, there might be
1000 documents with 5 pages each, or 5 documents with 1000 pages
each, or somewhere inbetween. Documents do not contain other documents.
In my xaml markup I have a ListBox, whose ItemsTemplate
references an innerItemsTemplate that also has a ListBox. I want the
2 levels of selected items so that I can perform various operations
on documents or pages (delete, merge, move to new location, etc).
The innerItemsTemplate ListBox uses a WrapPanel as the ItemsPanelTemplate.
For the scenario where I have a large number of documents with a few
pages each (say, 10000 documents with 5 pages each), the scrolling
works great thanks to the UI Virtualization by the VirtualizingStackPanel.
However, I have problems if I have a large number of pages. A document
with 1000 pages will only display about 50 at a time (whatever fits on the screen), and when I scroll down, the outer ListBox moves to the next document, skipping the 950
pages or so that were not visible. Along with that, there is no
VirtualzingWrapPanel so the app memory really increases.
I'm wondering if I am going about this the right way, especially
since it is sort of difficult to explain! I would like to be able to display
10000 documents with 1000 pages each (only showing whatever fits on the screen),
using UI Virtualization, and also smooth scrolling.
How can I make sure the scrolling moves through all of the pages in document
before it displays the next document, and still keep UI virtualization?
The scrollbar seems to only move to the next document.
Does it seem logical to represent "documents" and "pages" -
with my current method of using a ListBox within a ListBox?
I would very much appreciate any ideas you have.
Thank You.
It is possible to achieve smooth scrolling VirtualizingStackPanels in WPF 4.0 without sacrificing virtualization if you're prepared to use reflection to access private functionality of the VirtualizingStackPanel. All you have to do is set the private IsPixelBased property of the VirtualizingStackPanel to true.
Note that in .Net 4.5 there's no need for this hack as you can set VirtualizingPanel.ScrollUnit="Pixel".
To make it really easy, here's some code:
public static class PixelBasedScrollingBehavior
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior), new UIPropertyMetadata(false, HandleIsEnabledChanged));
private static void HandleIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var vsp = d as VirtualizingStackPanel;
if (vsp == null)
{
return;
}
var property = typeof(VirtualizingStackPanel).GetProperty("IsPixelBased",
BindingFlags.NonPublic | BindingFlags.Instance);
if (property == null)
{
throw new InvalidOperationException("Pixel-based scrolling behaviour hack no longer works!");
}
if ((bool)e.NewValue == true)
{
property.SetValue(vsp, true, new object[0]);
}
else
{
property.SetValue(vsp, false, new object[0]);
}
}
}
To use this on a ListBox, for example, you would do:
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel PixelBasedScrollingBehavior.IsEnabled="True">
</VirtualizingStackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
The answer here is surprising:
If you use ItemsControl or ListBox you will get the behavior you are experiencing, where the control scrolls "by item" so you jump over a whole document at once, BUT
If you use TreeView instead, the control will scroll smoothly so you can scroll through your document and into the next one, but it will still be able to virtualize.
I think the reason the WPF team chose this behavior is that TreeViewcommonly has items that are larger than the visible area, whereas typically ListBoxes don't.
In any case, it is trivial in WPF to make a TreeView look and act like a ListBox or ItemsControl by simply modifying the ItemContainerStyle. This is very straightforward. You can roll your own or just copy over the appropriate template from the system theme file.
So you will have something like this:
<TreeView ItemsSource="{Binding documents}">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<ContentPresenter /> <!-- put your desired container style here with a ContentPresenter inside -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<DataTemplate TargetType="{x:Type my:Document}">
<Border BorderThickness="2"> <!-- your document frame will be more complicated than this -->
<ItemsControl ItemsSource="{Binding pages}">
...
</ItemsControl>
</Border>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Getting pixel-based scrolling and ListBox-style multiselect to work together
If you use this technique to get pixel-based scrolling, your outer ItemsControl which shows the documents cannot be a ListBox (because ListBox is not a subclass of TreeView or TreeViewItem). Thus you lose all of ListBox's multiselect support. As far as I can tell, there is no way to use these two features together without including some of your own code for one feature or the other.
If you need both sets of functionality in the same control, you have basically several options:
Implement multi-selection yourself in a subclass of TreeViewItem. Use TreeViewItem instead of TreeView for the outer control, since it allows multiple children to be selected. In the template inside ItemsContainerStyle: Add a CheckBox around the ContentPresenter, template bind the CheckBox to IsSelected, and style the CheckBox with control template to get the look you want. Then add your own mouse event handlers to handle Ctrl-Click and Shift-Click for multiselect.
Implement pixel-scrolled virtualization yourself in a subclass of VirtualizingPanel. This is relatively simple, since most of VirtualizingStackPanel's complexity is related to non-pixel scrolling and container recycling. Dan Crevier's Blog has some useful infromation for understanding VirtualizingPanel.
.NET 4.5 now has the VirtualizingPanel.ScrollUnit="ScrollUnit" property. I just converted one of my TreeViews to a ListBox and the performance was noticeably better.
More information here: http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingpanel.scrollunit(v=vs.110).aspx
This worked for me. Seems a couple of simple attributes will do it (.NET 4.5)
<ListBox
ItemsSource="{Binding MyItems}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.ScrollUnit="Pixel"/>
Please allow me to preface this answer with a question: Does the user have to see each and every thumbnail within every item in the list at all times?
If the answer to that question is 'no', then perhaps it would be feasible to limit the number of visible pages within the inner item template (given that you have indicated the scrolling works well with, say, 5 pages) and use a separate 'selected item' template that is larger and displays all pages for that document? Billy Hollis explains how to 'pop' a selected item out in a listbox on dnrtv episode 115

Render List<Canvas> as list items (or itemscontrol)

I've effectively got a List or a List, XAML that represents the canvas elements... as a return from some stuff I have no control of.
I've been moderately successful rendering via stackpanel.children.add etc, however want to start having the list of canvas within a virtualizing panel or list.
I've set itemssource and datacontext on the <ItemsControl> and set the <DataTemplate> as such
<DataTemplate>
<ContentControl content="{Binding Path=CanvasBody}"/>
</DataTemplate>
This effectively turns entire silverlight body white/blank. I dont really care how I ultimately get to the desired result which is a list of the rendered canvas's... preferably virtualized for speed.
Its a retarded problem, and not ideal as far as how silverlight apps are built, I know...
I'd really appreciate any pointers. THANKS!
Generally to display a list of elements you bind the items control itemssource property to the list and then set a datatemplate for it which displays the desired properties of the list item type. You dont need to set content control. Beyond that I cant see what exactly you are asking.
<DataTemplate>
<Grid>
<TextBlock Text="{Binding ExampleProperty1}"/>
</Grid>
</DataTemplate>
codebehind file:
public class ExampleClass
{
public String ExampleProperty1
{
get
{
return "TEST";
}
}
}
public List<ExampleClass> List {get;} // note that this must be a public PROPERTY,
// not a field!

How to Clone a whole grid of Controls?

I have the following code and basically what i am not able to figure out is how to clone the whole grid and make a blank copy of them side by side.... for a clear understanding this is something to do with hospital application and the grid is related to a pregnancy so when said 'ADD CHILD' button a whole new grid should be created during run time, thanks for the help below is a link that might help people cause i tried it but not sure how to display it
How can you clone a WPF object?
You should put the object you are want to "clone" in a DataTemplate and reference this template from an ItemsControl, then when you need another grid add another item to the items control (or even better to the list the control is bound to) and the ItemsControl will create a new grid and bind it the new object.
For an example take a look at this post on my blog.
Here is an example for this application (I left only the relevant parts and I didn't test it, so there are probably some typos there):
<Window ... >
<Window.Resources>
<DataTemplate x:Key="ChildTemplate">
<Grid>
...
<TextBlock Text="Delivery Date:" Grid.Column="0" Grid.Row="0"/>
<TextBox Text="{Binding DeliveryDate}" Grid.Column="1" Grid.Row="0"/>
<TextBlock Text="Delivery Time:" Grid.Column="0" Grid.Row="1"/>
<TextBox Text="{Binding DeliveryTime}" Grid.Column="1" Grid.Row="1"/>
...
</Grid>
</DataTemplate>
</Window.Resources>
...
<Button Content="AddChild" Click="AddChildClick"/>
...
<ScrollViewer>
<ItemsControl ItemsSource="{Binding AllChildren}" ItemsTemplate="{StaticResource ChildTemplate}">
<ItemsControl.PanelTemplate>
<ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemPanelTemplate>
<ItemsControl.PanelTemplate>
</ScrollViewer>
...
</Window>
And in cs:
Set an object with all the form data as the Window's DataContext. I'll call this class PostDelveryData.
Create another class with the repeating data. I'll call it ChildDeliveryData.
Add a property of type ObservableCollection<ChildDeliveryData> called AllChildren to PostDeliveryData; it's important it'll be ObservableCollection and not any other type of collection.
Now, for the magic:
private void AddChildClick(object sender, RoutedEvetnArgs e)
{
((PostDeliveryData)DataContext).AllChildren.Add(new ChildDeliveryData());
}
And when you add the new item to the list another copy of the entire data template will be added.
I'm not sure that you're using the correct approach here. I would approach the problem by creating a "ChildGridControl" with a Child property, and let the Child property handle the databinding. Adding a new child to the GUI would involve creating a new instance of the ChildGridControl.
If I am understanding correctly, you should create a UserControl, which wraps your Grid and subsequent controls inside. And use this User control anywhere you wanted to replicate that UI.

Resources