Reputation: 309
I'm new to WPF but I've done C# for quite some time now and I am currently developing a simple window (Windows Desktop) that should visualize all photos in a directory. The application should also know about EXIF data like ISO, aperture and so on, for which I use a DLL.
I have defined a Photo
class:
public class Photo {
public string FileName { get; set; }
public int ISO { get; set; }
...
}
that I want to store in a List<Photo>
at runtime.
I've then declared a PhotoItem
(XAML User Control) with an Image control and a TextBlock on it. For every Photo
created there will be one PhotoItem
created that holds the corresponding Photo
as a property:
public partial class PhotoItem : UserControl {
...
public Photo Photo { get; set; }
...
}
From this Photo
property the PhotoItem
knows where to look for the image and what ISO etc. to display.
Now to my problem. Because it would take way too long to load the Image itself as well as the metadata already if the user selects the directory, I want to first add all the PhotoItem
s to the window (still empty) and then run the metadata lookup and Image thumbnail loading for each of them. Of course it would be best if these operations don't block the UI thread, hence I'm currently using one Task
for gathering metadata and one for gathering the thumbnail.
How would I make the PhotoItems update if metadata for the image is now available? Basically how can you have one centralized location where all data is stored, to which Tasks can provide updates and from which the UI thread can build information. I know a bit about Bindings in XAML/WPF, but binding e.g. a TextBlock's text to the Photo.ISO
variable would always display zero if the metadata was not gathered yet. In this case I would want to hide all the text detail on the PhotoItem
.
On the other hand I've also thought about implementing something like a 'Refresh' function inside the PhotoItem
, but that would reload the image and would take a long time (this was probably my favorite WinForms way to do it, haha).
Can anyone give me an idea of how to realize this?
Thanks in advance!
Upvotes: 1
Views: 3092
Reputation: 128060
Let's take a look at a basic example without a UserControl.
The first step is to create a view model to enable data binding. You would make the Photo class implement the INotifyPropertyChanged
interface to update bindings when property values change.
The class below also declares an Image
property that holds an ImageSource
derived object, which is loaded asynchronously.
public class Photo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
public string FileName { get; set; }
private string iso = string.Empty;
public string ISO
{
get { return iso; }
set
{
iso = value;
NotifyPropertyChanged(nameof(ISO));
}
}
private ImageSource image;
public ImageSource Image
{
get { return image; }
set
{
image = value;
NotifyPropertyChanged(nameof(Image));
}
}
public async Task Load()
{
Image = await Task.Run(() =>
{
using (var fileStream = new FileStream(
FileName, FileMode.Open, FileAccess.Read))
{
return BitmapFrame.Create(
fileStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
});
ISO = "1600";
}
}
The second part of the view model is a class that holds a collection of Photo
instances:
public class ViewModel
{
public ObservableCollection<Photo> Photos { get; }
= new ObservableCollection<Photo>();
}
For the typical data binding scenario, you would assign an instance of this class to the DataContext
of your Window, either in code or in XAML:
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
The last part is the declaration of the ListBox with a DataTemplate
that visualizes a Photo
:
<ListBox ItemsSource="{Binding Photos}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Image}" Width="100" Height="100"/>
<StackPanel>
<TextBlock Text="{Binding ISO, StringFormat=ISO: {0}}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Now you could fill the Photos
collection for instance in an asynchronous Loaded
event handler of the MainWindow like this:
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
var vm = (ViewModel)DataContext;
foreach (var file in Directory.EnumerateFiles(...))
{
vm.Photos.Add(new Photo { FileName = file });
}
foreach (var photo in vm.Photos)
{
await photo.Load();
}
}
Upvotes: 7