.NET 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

SkyDrive Sync on Windows Phone - Adding Local Content

08.30.2013
| 2360 views |
  • submit to reddit

In the previous installment in this series I was talking about creating the basic harness that would allow you to access SkyDrive from your application. Now it's time to get a little further and see how we can add local content that will be later synced.

As a sidenote, I would highly recommend testing this on a physical device rather than in the emulator. It would make a lot of things easier, since you will have more content options.

At this point, we have created the routine that initializes the root folder for the project (SYNCFOLDER). As a reminder, we will not push content inside the root SkyDrive folder, because the end-user will get easily confused with all the content mixed up. Besides, we don't want to sync content that was not originally pushed from a device (at least not yet).

That being said, we want the user to add content from the local device to some sort of a reference collection. Remember, that since this is a Windows Phone device, we have several options. First of all, the user can get pictures from the Media Library. Or he/she could get files from the attached external storage (SD card). I won't be talking about creating sync folders, but rather a single sync block - add any files, and those will be synced to a central location at once, regardless of where you got the item.

Let's begin by adding an ApplicationBar with a single menu item - "add file":

<phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar>
        <shell:ApplicationBar.MenuItems>
            <shell:ApplicationBarMenuItem Text="add file" Click="AddFile_Click"></shell:ApplicationBarMenuItem>
        </shell:ApplicationBar.MenuItems>
    </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>


But where would one add a file? Let's say that we have a custom model, that keeps a reference to a file, with its own type indicator:

namespace SkyDriveSyncSample.Models
{
    public class FileReference
    {
        public string Name { get; set; }
        public string Token { get; set; }
        public FileType FileType { get; set; }
    }
}

Here, FileType is an indicator of the originating source:

namespace SkyDriveSyncSample.Models
{
    public enum FileType
    {
        LocalAlbum,
        RemoteAlbum,
        ExternalStorage,
        IsolatedStorage
    }
}

There is a clear difference that you have to be aware of - pictures from local and remote (e.g. Facebook, SkyDrive) albums are retrieved through the same picker, but in different formats.

Moving on, we need to give the user an option to choose the location of the content needed to be synced. Let's say that for now the isolated storage is empty, so we have to potential choices - the media library and external storage.

To do this, we can use the XNA MessageBox:

private void AddFile_Click(object sender, System.EventArgs e)
{
    Guide.BeginShowMessageBox("SkyDrive Sync", "Select the source target for the content you want to add.",
        new List<string> { "media lib", "sd card" }, 0, MessageBoxIcon.Alert, new System.AsyncCallback(result =>
            {
                int? returnButton = Guide.EndShowMessageBox(result);

                if (returnButton == 0)
                {
                    // The user decided to use the media library.
                }
                else if (returnButton == 1)
                {
                    // The user decided to use the external storage.
                }
            }), null);
}

So now you gave the user the option to choose the location of the file content. Remember, however, that you will not be able to select content from external storage on every single Windows Phone device - only on those that support it (e.g. Nokia Lumia 820/825).


For the scenario where the user decides to use the media library, we can leverage the pretty obvious choice - PhotoChooserTask.

PhotoChooserTask task = new PhotoChooserTask();
task.Completed += (s, args) =>
    {
        // Task completed.
    };
task.ShowCamera = true;
task.Show();

When the task completes, we can create a new FileReference that would indicate the source and location of the selected file. Again, going back to the tricky part, specifically, take a look at these two paths, returned as the file name from the task:

  • C:\Data\Users\Public\Pictures\Saved Pictures\_pomf__Baconit.jpg
  • C:\Data\SharedData\Comms\Unistore\data\18\e\0000050400000018700b.dat
What do you think is the difference between these two, other than the location of the content? The first one is in a local album, while the second, although represented through a DAT extension, is an image that is taken from a remote album, such as SkyDrive (or Facebook).

Because there is no explicit identifier that tells us what album we're grabbing the picture from, we need to get that reference for local images, and just push the remote image into isolated storage (since we are not able to retrieve it through the path).

Doubling down on the Path class we can easily get the album name and the file name from the returned original file location:

task.Completed += (s, args) =>
    {
        var reference = new FileReference();
        
        if (args.OriginalFileName.StartsWith("C:\\Data\\Users"))
        {
            // We're dealing with a file from a local album.
            string album = Path.GetFileName(Path.GetDirectoryName(args.OriginalFileName));
            string name = Path.GetFileName(args.OriginalFileName);

            reference.Name = name;
            reference.Token = album;
            reference.FileType = FileType.LocalAlbum;
        }
        else
        {
            // We're working with a file from a remote album.
        }
    };

Notice that the FileReference instance will carry the album name as the token, by which we can later identify the image. 

When the file that we're working with is from a remote album, we just need to push the stream into a StorageFile that will be a part of the application data folder. For that, we have a helper function, so that I can transform an existing stream:

internal async static Task<string> WriteToFileFromReader(string fileName, DataReader reader)
{
    uint POTENTIAL_BUFFER_LENGTH = 256 * 1024;

    try
    {
        while (reader.UnconsumedBufferLength > 0)
        {
            Debug.WriteLine("UNCONS_BUFF_LEN: " + reader.UnconsumedBufferLength);

            IBuffer readBuffer = null;
            if (reader.UnconsumedBufferLength >= POTENTIAL_BUFFER_LENGTH)
            {
                readBuffer = reader.ReadBuffer(POTENTIAL_BUFFER_LENGTH);
            }
            else
            {
                readBuffer = reader.ReadBuffer(reader.UnconsumedBufferLength);
            }

            var folder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("SyncStorage",
                CreationCollisionOption.OpenIfExists);
            var imageFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists);


            using (IRandomAccessStream randomAccessStream = await imageFile.OpenAsync(FileAccessMode.ReadWrite))
            {
                using (IOutputStream outputStreamAt = randomAccessStream.GetOutputStreamAt(randomAccessStream.Size))
                {
                    await outputStreamAt.WriteAsync(readBuffer);
                    await outputStreamAt.FlushAsync();
                }
            }

        }

        Debug.WriteLine("UNCONS_BUFF_LEN_AFTEROP: " + reader.UnconsumedBufferLength);

        return fileName;
    }
    catch
    {
        return string.Empty;
    }
}

Going back to the task completion handler, here is how we push the stream into a file:

// We're working with a file from a remote album.
StorageFolder folder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("SyncContent", CreationCollisionOption.OpenIfExists);
string name = Path.GetFileName(args.OriginalFileName);

using (DataReader reader = new DataReader(args.ChosenPhoto.AsInputStream()))
{
    await reader.LoadAsync((uint)args.ChosenPhoto.Length);
    reference.Name = await WriteToFileFromReader(name, reader);
}

reference.FileType = FileType.RemoteAlbum;

We already know that the file will be stored in SyncStorage, therefore there is no need to set the token to anything. Marking the file type as remote album is enough.

The problem now comes with the fact that we don't really store the references anywhere. Let's create an ObservableCollection<FileReference> on the page.

DependencyProperty _fileReferenceProperty = DependencyProperty.Register("FileReferences",
    typeof(ObservableCollection<FileReference>), typeof(MainPage), new PropertyMetadata(new ObservableCollection<FileReference>()));
public ObservableCollection<FileReference> FileReferences
{
    get
    {
        return (ObservableCollection<FileReference>)GetValue(_fileReferenceProperty);
    }
    set
    {
        SetValue(_fileReferenceProperty, value);
    }
}

In the page XAML, we can now create a ListBox that will be bound to it, also displaying the name for each of the files that is added:

<ListBox Grid.Row="1" ItemsSource="{Binding FileReferences}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Name}" Style="{StaticResource PhoneTextTitle2Style}"></TextBlock>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

For the PhotoChooserTask completion, you can simply add this line to add the reference to the global collection:

FileReferences.Add(reference);

Congratulations, now you can track the added files visually: