Mobile Zone is brought to you in partnership with:

Den is a DZone Zone Leader and has posted 460 posts at DZone. You can read more from them at their website. View Full User Profile

Building A File Picker for Windows Phone 8

03.11.2013
| 11448 views |
  • submit to reddit

Windows Phone 8 SDK comes with an exceptional capability - read contents from an SD card. This ultimately allows you as the developer to read content that is not necessarily stored in the phone media library, and although the built-in phone storage is still locked, being able to read content from a removable device adds much more flexibility to your application when it comes to content generated outside the application itself.

One of the things that I noticed is the fact that there is no default file picker control that would allow you to explore the contents of the SD card, so I decided to create a very raw implementation that would allow me to pick a single file and return it to the core application.

Basics

Let's start by looking at the structure of the SD card folder layout:


Remember that there are stock folders that are unaccessible, such as:

  • WPSystem (hidden)
  • Music
  • Pictures
  • Videos
To clarify, if you look at the image above, in case I want to list all folders in the root of the SD card, I will see TestFolder, but not the other folders. This is by design and is used to protect the user's content, such as pictures and licensed audio tracks.

There is another limitation in place that you need to be aware of, and it is connected to the files that can be listed - those will be only items that have their file extension explicitly associated with the application. I've discussed this topic in one of the previous articles - feel free to take a look at it if you want to know how to register file extensions with your application (or you could read the MSDN article).

The mechanism I am using in the file picker implementation is extension-independent at this point, therefore as a developer you don't need to worry about fine-tuning it to display files of a certain extensions. 

Building Blocks

First, start by creating a new UserControl in your project. I named mine FileExplorer, but as you can tell, naming is flexible and doesn't really make a difference here. All contents inside the explorer will be placed inside a Popup, so in your XAML editor add a Popup wrap.
<Popup x:Name="RootPopup">
    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="90"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <Grid Margin="8">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="330"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
            </Grid.ColumnDefinitions>

            <TextBox IsReadOnly="True" Text="{Binding ElementName=FileExplorerControl, Path=CurrentPath}"></TextBox>
            <Button Grid.Column="1" x:Name="btnBack" Click="TreeUp">
                <Image Source="/Assets/Icons/up.png"></Image>
            </Button>
        </Grid>

        <ListBox Margin="8" Grid.Row="1" SelectionChanged="SelectionChanged" x:Name="lstCore" 
             ItemsSource="{Binding ElementName=FileExplorerControl, Path=CurrentItems}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Height="64" Width="64" VerticalAlignment="Center" Source="{Binding IsFolder, Converter={StaticResource ExplorerTypeToIconConverter}}"></Image>
                        <TextBlock VerticalAlignment="Center" Style="{StaticResource PhoneTextLargeStyle}" Foreground="White" Text="{Binding}"></TextBlock>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Popup>
Let's take a closer look at what's inside. Obviously, I have a content container - my Grid control. Inside it I have a ListBox with a template that consists of an icon (Image) and a label (TextBlock). Both are internally bound to two properties - IsFolder and the Name of the bound item. That is, both are bound to a FileExplorerItem instance:
public class FileExplorerItem
{
    public string Name { get; set; }
    public string Path { get; set; }
    public bool IsFolder { get; set; }

    public override string ToString()
    {
        return Name;
    }
}

And even though there is no problem displaying the name in a TextBlock, by default the image location cannot be inferred from the Boolean flag that determines whether the item is a folder or not. For that purpose, there is a simple converter:

public class ExplorerTypeToIconConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        bool isFolder = (bool)value;

        if (isFolder)
            return new Uri("/Assets/Icons/folder.png", UriKind.Relative);
        else
            return new Uri("/Assets/Icons/file.png", UriKind.Relative);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

In XAML, I can reference this converter, given that I declared the proper namespace:

<UserControl.Resources>
    <converters:ExplorerTypeToIconConverter x:Key="ExplorerTypeToIconConverter"></converters:ExplorerTypeToIconConverter>
</UserControl.Resources>

You can choose any icon to decorate the item - in my case, if the item is a folder, I am displaying a folder icon, and if it is not - I am displaying a generic document icon.

All items that are related to the current selection are contained in the CurrentItems ObservableCollection<FileExplorerItem> instance:

public ObservableCollection<FileExplorerItem> CurrentItems { get; set; }

Diving Into Code

Now let's look into what's going on behind the curtains. First of all, remember that the control can be called from any possible page, where we will need to hide the system tray and the application bar, as any other custom prompt that takes the entire screen (system tray is optional). Since the page is unknown, I need to somehow keep track of the current instance through the core frame, and for that inside the control class I have the following declarations:
PhoneApplicationPage _currentPage;
PhoneApplicationFrame _currentFrame;

At this point, these are left unassigned. Also, since I am handling external storage, I need an ExternalStorageDevice active instance:

ExternalStorageDevice _currentStorageDevice;

Same as with the page and the frame, this is yet to be initialized.

Other than CurrentItems, I also need a generic Stack to carry the folders that are navigated through. 

Stack<ExternalStorageFolder> _folderTree { get; set; }

Going back to your Data Structures course in college, a stack is a LIFO (last in, first out) data structure, in which you can push an item, and pop an item. It comes without the overhead of other generic lists, and without the complexity of a simple array, as you will need to resize it. When a user navigates to a folder deeper in the tree, the ExternalStorageFolder instance is pushed onto the stack. Going up the tree, a single folder is popped from the stack.

As the file picker is opened, I already mentioned that I might need to preserve the state of the application bar and the system tray, and for that I have two flags:

bool _mustRestoreApplicationBar = false;
bool _mustRestoreSystemTray = false;

When the file picker is dismissed, I need to trigger a custom event handler that will let the user work on the ExternalStorageFile instance that was selected:

public event OnDismissEventHandler OnDismiss;
public delegate void OnDismissEventHandler(ExternalStorageFile file);

Once the control is initialized, I also need to do additional initialization, that I can put in a single Initialize function:

async void Initialize()
{
    _folderTree = new Stack<ExternalStorageFolder>();
    CurrentItems = new ObservableCollection<FileExplorerItem>();

    var storageAssets = await ExternalStorage.GetExternalStorageDevicesAsync();
    _currentStorageDevice = storageAssets.FirstOrDefault();

    LayoutRoot.Width = Application.Current.Host.Content.ActualWidth;
    LayoutRoot.Height = Application.Current.Host.Content.ActualHeight;
    
    if (_currentStorageDevice != null)
        GetTreeForFolder(_currentStorageDevice.RootFolder);
}

In basic terms, I am initializing the handled collections, attempting to get the external storage device, forcing the height and the width to be equal to the screen size and getting the folder structure of the root folder with the help of the GetTreeFolder function (only if the storage device is not null):

async void GetTreeForFolder(ExternalStorageFolder folder)
{
    CurrentItems.Clear();

    var folderList = await folder.GetFoldersAsync();

    foreach (ExternalStorageFolder _folder in folderList)
    {
        CurrentItems.Add(new FileExplorerItem() { IsFolder = true, Name = _folder.Name, Path = _folder.Path });
    }

    foreach (ExternalStorageFile _file in await folder.GetFilesAsync())
    {
        CurrentItems.Add(new FileExplorerItem() { IsFolder = false, Name = _file.Name, Path = _file.Path });
    }

    if (!_folderTree.Contains(folder))
        _folderTree.Push(folder);

    CurrentPath = _folderTree.First().Path;
}

Here, I am transforming each folder and file into my bindable FileExplorerItem, and pushing the current folder onto the top of the stack, its path being the path displayed in the TextBox at the top of the popup that we have built prior.

What happens if I decide to go up the folder tree, navigating back to the previous folders? For that purpose, I have the TreeUp function:

void TreeUp(object sender, RoutedEventArgs e)
{
    if (_folderTree.Count > 1)
    {
        _folderTree.Pop();
        GetTreeForFolder(_folderTree.First());
    }
}

If the stack that I am working with contains only the root folder, obviously I cannot go up the tree, therefore I am blocking that possibility by checking the current count of items that are registered. Otherwise, I am popping the stack head and using the first available folder as the source to get the content tree for via GetTreeForFolder.

Now let's take a look at the flow that happens when the control (and inherently, the popup) is shown:

public void Show()
{
    _currentFrame = Application.Current.RootVisual as PhoneApplicationFrame;
    _currentPage = _currentFrame.Content as PhoneApplicationPage;

    if (SystemTray.IsVisible)
    {
        _mustRestoreSystemTray = true;
        SystemTray.IsVisible = false;
    }


    if (_currentPage.ApplicationBar != null)
    {
        if (_currentPage.ApplicationBar.IsVisible)
            _mustRestoreApplicationBar = true;

        _currentPage.ApplicationBar.IsVisible = false;
    }

    if (_currentPage != null)
    {
        _currentPage.BackKeyPress += OnBackKeyPress;
    }

    RootPopup.IsOpen = true;
}

I am grabbing the main application frame and getting the current page instance, which in this scenario acts as the FileExplorer caller. Once that is completed, I am checking whether I need to preserve the state of the system tray and the application bar, in case those are visible - when the FileExplorer instance is shown, both become hidden.

Also, we want to make sure that if the user pressed the back button while the popup is open, he is redirected back to the application page where he was working instead of just backing out of the application entirely or going back to the previous page. For that, we are going to suppress the back button (temporarily) by hooking our own BackKeyPress event handler:

void OnBackKeyPress(object sender, CancelEventArgs e)
{
    e.Cancel = true;
    Dismiss(null);
}

Notice that here I have a Dismiss function call, which is invoked each time the FileExplorer dialog is closed.

private void Dismiss(ExternalStorageFile file)
{
    if (_currentPage != null)
    {
        _currentPage.BackKeyPress -= OnBackKeyPress;
    }

    RootPopup.IsOpen = false;

    if (_mustRestoreApplicationBar)
        _currentPage.ApplicationBar.IsVisible = true;

    if (_mustRestoreSystemTray)
        SystemTray.IsVisible = true;

    if (OnDismiss != null)
        OnDismiss(file);
}

Its purpose is to revert to the initial page state (both for the system tray and the application bar) and to return the selected file, if any via the standard OnDismiss event handler, if one was associated. Remember to unhook the back button event handler to avoid unexpected application behavior. But where exactly is the file selected?

I am simply handling the selection change in the ListBox:

async void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (lstCore.SelectedItem != null)
    {
        FileExplorerItem item = (FileExplorerItem)lstCore.SelectedItem;
        if (item.IsFolder)
        {
            GetTreeForFolder(await _folderTree.First().GetFolderAsync(item.Name));
        }
        else
        {
            ExternalStorageFile file = await _currentStorageDevice.GetFileAsync(item.Path);
            Dismiss(file);
        }
    }
}

If I detect that the selected item type is a folder, I need to get its contents, hence the GetTreeForFolder call. Otherwise, it is a file and I need to get the actual ExternalStorageFile instance and call Dismiss with it as an argument.

What's the end result? This:




Simple, and gets the job done. The cool thing about this is that whenever I am getting the list of files, those are automatically linked to the associated file extensions. Your application can only read PDF and RAR files? That's what FileExplorer will show, no changes needed, thanks to the folks at Microsoft that implemented ExternalStorageFolder.GetFilesAsync().

Comments

Ahmed IG replied on Mon, 2013/07/15 - 12:50pm

Hello,

Is there any downloadable demo for this?

Lam Van Quoc Huy replied on Wed, 2014/04/02 - 12:38am

Hello,

I have create project follow this tuts but didn't show any thing when I debug. Please help me!

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.