Showing posts with label Files APIs. Show all posts
Showing posts with label Files APIs. Show all posts

Sunday, March 18, 2012

File searching and filtering


In my last post, I discussed accessing and displaying file data from your Metro app, and grouping files using the pivot API. Let me follow up on that with a discussion of basic file filtering using the search API. So in our photo browser app, suppose we’d like the user filter photos by their own rating. In the Windows OS, the user can right click on a picture and set a personal rating using the Rating property :
image
This is the property that we’re going to use for our filtering example. So we’ll assume the user has at least some pictures rated using the above property settings in her file system.
Here’s what we want to achieve :
image
The buttons in the top row match each distinct rating value the user has assigned to her pics. So we need to generate these buttons. Then, when the user clicks one of them, the list will refresh to only show pics matching the selected rating value.
Let’s start with the buttons :
   1: private async void CreateRatingButtons()
   2: {
   3:     var pivot = CommonFolderQuery.GroupByRating;
   4:     var folders = await library.GetFoldersAsync(pivot);
   5:  
   6:     var ratingValues = folders.Cast<StorageFolder>().Select(
   7:         f => new Rating { Name = f.Name }).OrderBy(r => r.Name);
   8:     Ratings = new ObservableCollection<Rating>(ratingValues);
   9:     DefaultViewModel["Ratings"] = Ratings;
  10: }

You can see our old friend the pivot, which we first use to group the files by rating.  As in the previous post, we generate a list of virtual folders, each representing a group of pictures matching a specific rating value. Then we just retrieve the vfolder name which is the rating value itself. At this point, our goal is just to get the list of rating values assigned by the user in her file system. We store them in a simple data model class in an observable collection to make data binding easier. Then we bind an items control to the collection to generate the list of buttons :


   1: <ItemsControl x:Name="icRatings"
   2:               ItemsSource="{Binding Ratings}">
   3:     <ItemsControl.ItemsPanel>
   4:         <ItemsPanelTemplate>
   5:             <StackPanel Orientation="Horizontal" />
   6:         </ItemsPanelTemplate>
   7:     </ItemsControl.ItemsPanel>
   8:     <ItemsControl.ItemTemplate>
   9:         <DataTemplate>
  10:             <Button Name="btnFilter" 
  11:                     Content="{Binding Name}"
  12:                     Click="btnFilter_Click" />
  13:         </DataTemplate>
  14:     </ItemsControl.ItemTemplate>
  15: </ItemsControl>

So the ItemsControl is bound to the Ratings list (here through the DefaultViewModel property for convenience, but you could just set icRatings’s ItemsSource to Ratings in code if you so prefer). That takes care of creating our filter buttons.

Next, what happens when a button is clicked ? Well, here’s the workhorse method :


   1: private async Task GetFilesAsync(StorageFolder rootFolder, string search = "")
   2: {
   3:     var options = new QueryOptions { FolderDepth = FolderDepth.Deep };
   4:     options.ApplicationSearchFilter = search;
   5:  
   6:  
   7:     var result = rootFolder.CreateFileQueryWithOptions(options);
   8:     var files = await result.GetFilesAsync();
   9:  
  10:     foreach (var file in files)
  11:     {
  12:         var myFile = new MyFile { FileName = file.Name };
  13:  
  14:         myFile.Thumbnail = await file.GetThumbnailAsync(ThumbnailMode.PicturesView, 200);
  15:  
  16:         var props = await file.Properties.RetrievePropertiesAsync(new List<string> { "System.RatingText", "System.Title" });
  17:         var title = props["System.Title"];
  18:         var ratingText = props["System.RatingText"];
  19:         myFile.Title = title != null ? title.ToString() : string.Empty;
  20:         myFile.RatingText = ratingText != null ? ratingText.ToString() : string.Empty;
  21:  
  22:         myFiles.Add(myFile);
  23:     }
  24: }

I start by creating a QueryOptions instance, and set its ApplicationSearchFilter to the filter criterion we’re interested in. In this case, search should be set in the button click handler :


   1: private async void btnFilter_Click(object sender, RoutedEventArgs e)
   2: {
   3:     myFiles.Clear();
   4:     
   5:     var rating = (sender as Button).DataContext as Rating;
   6:     if (rating != null)
   7:     {
   8:         ...
   9:         await GetFilesAsync(library, "System.Rating:" + ratingText);
  10:     }
  11: }

Note that I also set FolderDepth to Deep so as to specify a deep search. As we’ve seen before, I then create the query and start the async operation to retrieve the files matching the passed in search criterion. Then I loop through the result files, and seek to retrieve the actual rating value for the file (although I already know it since it should match the search criterion) as well as the image title. This is to illustrate accessing file properties using the new set of interfaces in WinRT Consumer Prev. Notice I first access the file’s Properties value and call RetrievePropertiesAsync() on that object, passing to it a list of property names I need to retrieve. The property names to pass are very badly documented at this point, but after some digging I found they must conform to Windows’ Advanced Query Syntax (documented here).  Here’s a list of Windows properties : http://msdn.microsoft.com/en-us/library/bb760685(VS.85).aspx

So anyway to make things short, the property we’re interested in is not System.Rating, which returns a number, but System.RatingText. So we retrieve that as well as Title, and we store the retrieved values in our MyFile data model object so we can bind to it in the UI. Here’s the updated version of MyFile :


   1: public class MyFile : BindableBase
   2: {
   3:     public string FileName { get; set; }
   4:  
   5:     string title;
   6:     public string Title 
   7:     {
   8:         get { return title; }
   9:         set { SetProperty<string>(ref title, value); }
  10:     }
  11:  
  12:     public StorageItemThumbnail Thumbnail { get; set; }
  13:     public string RatingText { get; set; }
  14: }

So hopefully, this has given you an idea of how you can implement searching/filtering on file data in your apps. I’ve used a simple example but you can obviously implement much more complex scenarios using these techniques.

Happy filtering…

Using files as your app data


In Windows 8, you can use your files as data for your app. Suppose you have a bunch of pics stored on your machine or on your network, for example. You can easily display them in your Metro app :
image
Here’s the basic method behind this UI :
   1: private async Task GetFilesAsync(IStorageFolder rootFolder)
   2: {
   3:     myFiles.Clear();
   4:  
   5:     var files = await rootFolder.GetFilesAsync();
   6:  
   7:     foreach (var f in files)
   8:     {
   9:         var myFile = new MyFile { FileName = f.Name };
  10:  
  11:         myFile.Thumbnail = await (f as StorageFile).GetThumbnailAsync(
  12:             ThumbnailMode.ListView, 100, ThumbnailOptions.ResizeThumbnail);
  13:  
  14:         myFiles.Add(myFile);
  15:     }
  16: }


It uses a data model file that looks something like this :


   1: public class MyFile : BindableBase
   2: {
   3:     public string FileName { get; set; }
   4:  
   5:     string title;
   6:     public string Title 
   7:     {
   8:         get { return title; }
   9:         set { SetProperty<string>(ref title, value); }
  10:     }
  11:  
  12:     public StorageItemThumbnail Thumbnail { get; set; }
  13: }

A custom file class that stores the file name, title (set by the user on her pictures) and thumbnail to display. Note it extends BindableBase for convenience to easily implement INotifyPropertyChanged and raise UI notifications but simply calling SetMethod(), defined in BindableBase.

Also note the GetFilesAsync method asynchronously retrieves the thumbnail of each pic by calling GetThumbnailAsync. How easy is that ? Thanks winRT.

Now suppose you want to group the pics in some way. You can do it easily through the pivot API, which creates virtual folders that represent groups. Here’s another method :


   1: private async Task GetVirtualFoldersAsync(StorageFolder rootFolder)
   2: {
   3:     myVirtualFolders.Clear();
   4:  
   5:     var pivot = CommonFolderQuery.GroupByMonth;
   6:     var queryOptions = new QueryOptions(pivot);
   7:  
   8:     var queryResult = rootFolder.CreateFolderQueryWithOptions(queryOptions);
   9:     var vFolders = await queryResult.GetFoldersAsync();
  10:  
  11:     foreach (StorageFolder vfolder in vFolders)
  12:     { 
  13:         var myFolder = new MyFolder { FolderName = vfolder.Name };
  14:  
  15:         var files = await vfolder.GetFilesAsync();
  16:         myFolder.ImageCount = files.Count();
  17:  
  18:         myFolder.Thumbnail = await (vfolder as StorageFolder).GetThumbnailAsync(
  19:             ThumbnailMode.ListView, 200, ThumbnailOptions.ResizeThumbnail);
  20:  
  21:         myVirtualFolders.Add(myFolder);
  22:     }
  23: }


What this method does is generate a list of virtual folders each matching a group of pics taken during a given month. The pivot is defined using the GroupByMonth query, which will result in the files being pivoted on using this criterion. We could have picked another pivot, such as CommonFolderQuery.GroupByAuthor for example.

Next we call CreateFolderQueryWithOptions(options) to actually create the pivoted query. This method is available on the StorageFolder passed into the method, for example Windows.Storage.KnownFolders.PicturesLibrary.  Then we call GetFoldersAsync to actually dig and retrieve the files grouped by month.

Once the method completes, you can access the list of virtual folders representing the groups created. Here this is done in the foreach loop which loops over the virtual folders. My code then goes on to do app-specific stuff, such as retrieving the list of files in the group (vfolder) so as to display the count in the UI, and retrieve the vfolder’s thumbnail. Windows does that for you, it will generate a thumbnail for your folder or virtual folder, usually the thumbnail of the first pic in the folder. So you can call GetThumbnailAsync on a StorageFile OR a StorageFolder. Awesome.

Like before, I instantiate a custom class to store the vfolder data and store it in an observable collection so my UI can bind to it and get change notified. The UI then lists groups (vfolders), and I can add code so that when a Vfolder thumbnail is tapped, the list of actual pics in that vfolder gets displayed.

image

So you can see how easy it is to use files as app data and then implement simple pivots on that data. The beauty of this is the way we perform asynchronous operations, which would have previously made a hellish mess in our code, so smoothly and easily using async / await. Thanks to this new pattern the work we need to do to define and implement pivots on top of file resources in our data apps is almost fun!

Happy pivoting