已答复 C# Metro - Displaying images in a ListView or GridView

  • Saturday, July 28, 2012 5:17 AM
     
      Has Code

    I'm pulling my hair out trying to add images to my GridView, I've been googling for the last 6 hours, non-stop and I just can't seem to get my answer.

    I've found some code that seems to work, but sometimes the image will be blank. Another user said it was because the image was being loaded with a stream. I've tried everything I just can't find an elegant solution for a GridView populated by a folder full of images.

    Here is my code:

    public async Task getPhotos2()
            {
                var folder = await KnownFolders.PicturesLibrary.CreateFolderAsync("Mobile Contractor\\Clients\\" + App.activeclient.ClientID, CreationCollisionOption.OpenIfExists);
                IReadOnlyList<IStorageFile> files = (IReadOnlyList<IStorageFile>)await folder.GetFilesAsync(); //reads in all files from current working directory
    
                List<StackPanel> imagelist = new List<StackPanel>();
                int x = 0;
                foreach (StorageFile file in files) 
                {
                    IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
                    BitmapImage im = new BitmapImage();
                    im.SetSource(fileStream);
    
                    Image i = new Image();
                    i.Source = im;
    
                    StackPanel sp = new StackPanel();
                    TextBlock tb = new TextBlock();
    
                    tb.Text = file.Name;
                    sp.Children.Add(i);
                    sp.Children.Add(tb);
                    imagelist.Add(sp);
                }
                clientPictureViewer.ItemsSource = imagelist;
            }

All Replies

  • Saturday, July 28, 2012 6:55 AM
     
     Answered Has Code

    "I just can't find an elegant solution for a GridView populated by a folder full of images." ... Please find the way I'd do it:

    ---

    <Page
        x:Class="App2.MainPage"
        IsTabStop="false"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:App2"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">
        <Page.Resources>
            <ResourceDictionary Source="Common/StandardStyles.xaml"/>
        </Page.Resources>
        <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
            <GridView Grid.Row="0" Grid.Column="0" 
        		ItemsSource="{Binding ItemList}"
        		SelectionMode="Single">
                <GridView.ItemContainerStyle>
                    <Style TargetType="GridViewItem">
                        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        <Setter Property="VerticalContentAlignment" Value="Top"/>
                        <Setter Property="HorizontalAlignment" Value="Stretch"/>
                        <Setter Property="VerticalAlignment" Value="Top"/>
                    </Style>
                </GridView.ItemContainerStyle>
                <GridView.ItemTemplate>
                    <DataTemplate>
                        <Grid Background="Red" HorizontalAlignment="Stretch">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="20"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Image Grid.Column="0" Height="40" Width="40" Source="{Binding Image}" Stretch="UniformToFill"/>
                            <TextBlock Grid.Column="1" Text="{Binding Id}" TextAlignment="Right"/>
                            <TextBlock Grid.Column="2" Text="{Binding Title}" HorizontalAlignment="Stretch"/>
                        </Grid>
                    </DataTemplate>
                </GridView.ItemTemplate>
            </GridView>
        </Grid>
    </Page>

    ---

    using Windows.UI.Xaml.Controls;
    
    // The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238
    
    namespace App2
    {
        public sealed partial class MainPage : Page
        {
            KiwiExec exec;
            public MainPage()
            {
                this.DataContext = this.exec = new KiwiExec();
                this.exec.Init();
                this.InitializeComponent();
            }
        }
    }

    ---

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using Windows.Storage;
    
    namespace App2
    {
        public class KiwiExec : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void OnPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
    
            private ObservableCollection<KiwiItem> itemList = new ObservableCollection<KiwiItem>();
            public ObservableCollection<KiwiItem> ItemList
            {
                get { return itemList; }
                set
                {
                    itemList = value;
                    OnPropertyChanged("ItemList");
                }
            }
    
            /// <summary>
            /// !!!
            /// !!! ADJUST TO YOUR FOLDER NAME CONTAINING THE PICTURES
            /// !!! MODIFY PACKAGE MANIFEST TO ACCESS PICTURES AT RUN-TIME
            /// !!!
            /// </summary>
            private string PICTURE_FOLDER = "Clients\\MyClient";
    
            public async void Init()
            {
                // Variables
                StorageFolder folder;
                KiwiItem item;
                IReadOnlyList<IStorageFile> files;
                int offset = 1;
    
                // Exec
                folder = await KnownFolders.PicturesLibrary.CreateFolderAsync(PICTURE_FOLDER, CreationCollisionOption.OpenIfExists);
                files = (IReadOnlyList<IStorageFile>)await folder.GetFilesAsync();
                foreach (StorageFile file in files)
                {
                    item = new KiwiItem { Title = String.Format(" Title:{0}", file.Name), Id = offset++ };
                    item.SetImage(file);
                    this.ItemList.Add(item);
                }
            }
        }
    }

    ---

    using System;
    using System.ComponentModel;
    using Windows.Storage;
    using Windows.Storage.Streams;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Xaml.Media.Imaging;
    
    namespace App2
    {
        /// <summary>
        /// 
        /// </summary>
        public class KiwiItem : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void OnPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
    
            int id;
            public int Id
            {
                get { return id; }
                set { id = value; OnPropertyChanged("Id"); }
            }
    
            string title;
            public string Title
            {
                get { return title; }
                set { title = value;
                    OnPropertyChanged("Title"); }
            }
            private ImageSource image = null;
            public ImageSource Image
            {
                get { return this.image; }
    
                set { this.image = value; this.OnPropertyChanged("Image"); }
            }
    
            public async void SetImage(StorageFile file)
            {
                IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
                BitmapImage bi = new BitmapImage();
                bi.SetSource(fileStream); // or : await bi.SetSourceAsync(fileStream);
                Image = bi;
            }
        }
    }

    ---

    This is obviously an entry-level example only that is meant to point you in the 'right direction'. Let me provide you with another post that gives additional information: http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/0b290333-5170-48e5-ac40-ad3fa4f4dc72 

    For the benefit of the community, please return any drawbacks you might experience with this sample.




  • Saturday, July 28, 2012 9:24 PM
     
     

    Thank you!
    I haven't had the chance to try this out yet but I will post my results once I do.

    I am very new to xaml, and I hate markup languages entirely. I've tried to go as long as I could without having to learn it but it seems like I have run out of time.
    Can you please explain to me how data binding works?

    Specifically this line right here:
     <GridView Grid.Row="0" Grid.Column="0"
       
    ItemsSource="{Binding ItemList}"

    "ItemList" is defined in your code as a ObservableCollection<KiwiItem>

    KiwiItem is a class that is going to store the data we want to have displayed on the gridview, correct?
    So in our case, an image and a title.

    How does the xaml know that ItemList is in our code? Where do we make the references?

    Then in this part of the xaml:
    <Image Grid.Column="0" Height="40" Width="40" Source="{Binding Image}" Stretch="UniformToFill"/>
                           
    <TextBlock Grid.Column="1" Text="{Binding Id}" TextAlignment="Right"/>
                           
    <TextBlock Grid.Column="2" Text="{Binding Title}" HorizontalAlignment="Stretch"/>

    You are binding the source and text attributes to properties of KiwiItem, how does the xaml know that KiwiItem contains these properties?
    Is it because our grid itemsource is ItemList?

    I am just confused as to how I am supposed to reference these bindings if they are not declared anywhere within the xaml.

    If you can shed some light on this topic that would be great, thanks.

  • Saturday, July 28, 2012 10:11 PM
     
     Answered Has Code

    In Main Page class definition, the DataContext of the page is set. In ForInfo's sample, he set the Data Context as follows

      this.DataContext = this.exec = new KiwiExec();
    

    Now all controls on Main page, can access the DataContext. All controls on Main Page can be bound to public properties of KiWiExec class. When ForInfo set the ItemSource,

    <GridView Grid.Row="0"Grid.Column="0"
       
    ItemsSource="{Binding ItemList}"

    WinRT will look for DataContext on the Grid. We didn't set any DataContext on the grid. Then WinRT traverses on step up and looks to see if a DataContext is set on the Grids parent (which is the Page). In our case it will find that the DataContext has been set on the Page. Now it will check if there is public property called ItemList in the DataContext (which is set to the KiwiExec). In our case it finds it and then uses that list as the source of GridView.

    Now each Kiwiitem in the list becomes the DataContext for one Grid View item. You can apply the same theory as above to see how it finds the other properties.

    • Marked As Answer by MJFara Saturday, July 28, 2012 10:51 PM
    • Unmarked As Answer by MJFara Sunday, July 29, 2012 8:20 PM
    • Marked As Answer by MJFara Monday, July 30, 2012 2:18 PM
    •  
  • Saturday, July 28, 2012 10:54 PM
     
     

    Thank you very much for that explanation, I finally understand it!

    You both have been a great help, I used the code posted above and adjusted it to my scenario and it is working perfectly.
    Images are always being loaded now and are no longer showing up blank.

    I marked both of your responses as answers, thanks again!

  • Sunday, July 29, 2012 8:02 PM
     
     

    Unfortunately there are still random times where an image will not show. It just doesn't make any sense to me.

    Any ideas?

  • Monday, July 30, 2012 2:14 AM
     
     

    Bump!

  • Monday, July 30, 2012 2:36 AM
     
     
    What's the source of these images ? Is it local or web ?
  • Monday, July 30, 2012 6:29 AM
     
      Has Code

    [Test context: 4 images from a local folder]

    Looking for an answer, I made the following adjustment in the original solution:

            public async void SetImage(StorageFile file)
            {
                IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
                fileStream.Seek(0); // probably missing in the original solution
                BitmapImage bi = new BitmapImage();
                bi.SetSource(fileStream);
                Image = bi;
            }

    However, there were indeed still sporadic cases of a missing image. In those cases, the symptoms are - under debug -:

    - the Kiwi.Image.Getter of the said image is being invoked.
    - the said image is definitely not being opened, resulting in its PixelHeight and PixelWidth remaining 0 ('missing image')
    - there is no ImageFailed event, which is surprising
     -in most (but not all) of these cases there is a spurious message in the Output:

    'App1.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Threading\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Threading.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.

    ---

    Recoding the MainPage code-behind as follows resulted in the same spurious problems and symptoms:

    public sealed partial class MainPage : Page
        {
            KiwiExec exec;
            public MainPage()
            {
                this.DataContext = this.exec = new KiwiExec();
                // this.exec.Init(); // Moved to 'OnNavigatedTo'
                this.InitializeComponent();
            }
    
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                base.OnNavigatedTo(e);
                this.exec.Init();
            }
        }

    ---

    Finally, using the following code, I could not fault the system. Could you try it, thanks?

        public sealed partial class MainPage : Page
        {
            KiwiExec exec;
            public MainPage()
            {
                this.DataContext = this.exec = new KiwiExec();
                // this.exec.Init(); // Moved to 'Application.OnLaunched'
                this.InitializeComponent();
            }
        }
    
    /// <summary>
            /// Place the following code at the end of method 'OnLaunched' in App.xaml.cs.
            /// </summary>
    	protected override void OnLaunched(LaunchActivatedEventArgs args)
            {
                // ... standard code
    	    //
                // Load data here 
                {
                    MainPage mainPage = rootFrame.Content as MainPage;
                    KiwiExec exec = mainPage.DataContext as KiwiExec;
                    IAsyncAction aa = rootFrame.Dispatcher.RunAsync(CoreDispatcherPriority.Low, new DispatchedHandler(() => { exec.Init(); }));
                }
            }






  • Monday, July 30, 2012 2:17 PM
     
     

    Thanks for the response, it doesn't quite work or me to put the Init() in app.xaml.cs.

    The scenario is, I have a list of clients, all with a different set of images, therefore I need to call Init(baseobject objin) when the client selection is changed.
    It will look in the folder associated with that object.

    Edit: However, I THOUGHT I fixed the issue, although I'm not sure if it's fixed or more of a duct tape sort of thing. I added a 10 ms delay (Task.Delay(10)) right after I call SetImage, I guess this gives it enough time to load. I haven't had a problem until now, 1 picture failed to load.

    This issue is driving me crazy.

    The source of the images are local.


    • Edited by MJFara Monday, July 30, 2012 2:22 PM mistake
    •  
  • Monday, July 30, 2012 2:34 PM
     
     

    Looks like the delay doesn't help much at all, just had an attempt with 10 missing images.

    =(

  • Monday, July 30, 2012 3:23 PM
     
      Has Code

    I am equally confused ... All I can add to the above is that I have seen a similar case (in RP) where the developer would - in the PresentationLayer   - use the image within a UserControl where it is bound to the DataLayer 'Image'. So far so good.

    However, he would then - on UserControl.ImageDependencyProperty PropertyCallback - explicitly invoke the Image Getter within the change event and ... hold your breath ... set explicitly the Image.Source property there.

    I am not suggesting that you would use this, though ! I'd rather have someone discover a flaw in my example code above.

    public sealed partial class MyUserControl : UserControl
        {
            public static object GetBitmapSource(DependencyObject d) { return d.GetValue(BitmapSourceProperty); }
            public static void SetBitmapSource(DependencyObject d, object value) { d.SetValue(BitmapSourceProperty, value); }
            public static readonly DependencyProperty BitmapSourceProperty = DependencyProperty.Register("BitmapSource", typeof(BitmapImage), typeof(MyUserControl),
                    new PropertyMetadata(null, OnBitmapSourceChanged));
            private static void OnBitmapSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                MyUserControl ctrl = d as MyUserControl;
                BitmapImage bi = e.NewValue as BitmapImage;
                ctrl.myImage.Source = bi;
            }
        }

  • Monday, July 30, 2012 5:21 PM
     
     

    In your example code above, is there a way I can use that method dynamically when the selection is changed?
    Instead of when the app is being loaded?

    I'm hoping this is a bug and will get fixed by launch:(

  • Tuesday, July 31, 2012 6:08 AM
     
      Has Code

    There are many ways to do this.

    1.- The canonical way to handle the 'single selection' / 'currency' scenario is documented in http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/9d5e1433-fbc4-4236-96c9-89cc0467bac4/#478b885e-7152-4528-943d-6e5138a71fe5 I would suggest you familiarize yourself with that one in a separate project, by enumerating the client folders. Probably you don't need the MoveToFirst/Last/Previous/Next aspects though.

    2.- Once you have done that the question remaining is how to make the above cooperate in a browse/display fashion. The key code for that linkage is below:

    - in View_CurrentChanged you set the Current to a KiwiExec that you instantiate for the selected client; you perform the Init as well
    - you set the Current to that KiwiExec instance
    - you bind a ContentControl on your MainPage (e.g.) to 'Current': its DataContext is now the KiwiExec value of Current
    - your ContentControl now navigates (as in the solution above) the images of that client. 

    3.- I also provide a 'help feature' with respect to asserting the DataContext you have in the PresentationLayer: see http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/74badd45-0305-4ab0-8643-b69905e73145 . This is quite helpful when navigating thru several FrameworkElements (in your case the DataContext for the MainPage with the Folder Listview and the DataContext for the ContentControl (e.g.)) and not being sure of the DataContext these FrameworkElements points to.

    void View_CurrentChanged(object sender, object e)
            {
                Current = this.View.CurrentItem as KiwiItem;
            }
            private KiwiItem current;
            public KiwiItem Current
            {
                get { return current; }
                set
                {
                    current = value;
                    OnPropertyChanged("Current");
                }
            }
    

  • Tuesday, July 31, 2012 1:20 PM
     
      Has Code

    While your solution is more elegant, I have something similar implemented by firing an event when the selection of the ListView is changed.

    What I meant was this part specifically:

    protected override void OnLaunched(LaunchActivatedEventArgs args) { // ... standard code // // Load data here { MainPage mainPage = rootFrame.Content as MainPage; KiwiExec exec = mainPage.DataContext as KiwiExec; IAsyncAction aa = rootFrame.Dispatcher.RunAsync(CoreDispatcherPriority.Low, new DispatchedHandler(() => { exec.Init(); })); } }


    Am I able to put that where the event is getting fired?

  • Tuesday, July 31, 2012 2:07 PM
     
     

    I was afraid of that :-). Anyhow, given your design choice, the answer is yes:

    1.- you put the code in the corresponding Click _or_ Select event handler (you have two options), where you can look at the method signature to understand how to access the desired information (usually in the 'EventArgs')
       - the key point is to grab the DataContext of the Clicked _or_ Selected ListViewItem ->DataContext-> KiwiFolderItem
       - that KiwiFolderItem has a Name property that allows you to instantiate a KiwiExec with the appropriate folder name.
     
    2.- you set the instantiated KiwiExec in the KiwiFolderExec, in a similar manner to 2.- above, i.e. on a Property akin to 'Current' that binds with the presentation layer ContentControl: since it has property notification, the data context of your ContentControl will change accordingly and display the ObservableCollection<KiwiItem> of KiwiExec. There is a complete analogy with the 'Currency Management' in step 2. of the answer above.

    For the time being, comment out the 'OnLaunched' 'Load data here' code. You will reactivate it once your app works OK in order to display the last accessed folder, when your application resumes after suspend. But leave that for later: so implement first 1.- and 2.-