Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
259 views
in Technique[技术] by (71.8m points)

c# - How to load and show images asynchronously

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 PhotoItems 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!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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();
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...