This article explains how to let a Windows Store app manage a list of files in a working folder on the user’s OneDrive, without continuously opening file pickers. Some apps need to store more personal user data than the roaming folder can handle. A folder on the user’s OneDrive is a nice place to store that data -e.g. as flat files containing serialized business objects- and share it across his devices. Your users installed your app through the Store, so they all have a Microsoft account. And every Microsoft account comes with a OneDrive folder somewhere in the Cloud. So why not make use of it?
I made a little app that shows you how to log on to OneDrive, create a working folder for your app, upload a file, enumerate the files in the folder, and read the content of a file. Here’s how it looks like:
An HTTP client and some JSON magic would suffice to communicate with the OneDrive services directly, but the Live SDK for Windows, Windows Phone and .NET comes with an object model that does this for you.
You can either locally install it, and then make a reference to it in your project:
Or you can get it all through Nuget:
Under the hood this SDK of course still calls the REST service through HTTP, so you have to activate the Internet capability for your app (and make sure you publish a privacy policy):
Before you can use the API, your project needs to be associated with an app that is defined (not necessarily published) in the Store. This will update the manifest, and add an association file:
If you don’t associate your project with a Store app, then you may expect an exception:
Before your user can access his OneDrive through your app, he or she needs to be authenticated. The following code calls the LiveAuthClient.LoginAsync method, which takes the list of scopes as a parameter. The scopes for this particular call include single sign-on, and read and write access to the OneDrive:
private async void SignIn_Executed()
{
if (!this.isSignedIn)
{
try
{
LiveAuthClient auth = new LiveAuthClient();
var loginResult = await auth.LoginAsync(new string[] { "wl.signin", "wl.skydrive", "wl.skydrive_update" });
this.client = new LiveConnectClient(loginResult.Session);
this.isSignedIn = (loginResult.Status == LiveConnectSessionStatus.Connected);
await this.FetchfolderId();
}
catch (LiveAuthException ex)
{
Debug.WriteLine("Exception during sign-in: {0}", ex.Message);
}
catch (Exception ex)
{
// Get the code monkey's attention.
Debugger.Break();
}
}
}
When the call executes, the system sign-in UI opens, unless the user has already signed into his Microsoft account and given consent for the app to use the requested scopes. In most situations, your end user is already logged in and will never have to type his user id and password, and he would see this consent screen only once:
The app then needs to create a working folder. I decided to just use the full name of the app itself as the name of the folder:
public string FolderName
{
get { return Package.Current.Id.Name; }
}
Here’s how to create the folder -in the root of the user’s OneDrive- using a LiveConnectClient.PostAsync call:
private async void CreateFolder_Executed()
{
try
{
// The overload with a String expects JSON, so this does not work:
// LiveOperationResult lor = await client.PostAsync("me/skydrive", Package.Current.Id.Name);
// The overload with a Dictionary accepts initializers:
LiveOperationResult lor = await client.PostAsync("me/skydrive", new Dictionary<string, object>() { { "name", this.FolderName } });
dynamic result = lor.Result;
string name = result.name;
string id = result.id;
this.FolderId = id;
Debug.WriteLine("Created '{0}' with id '{1}'", name, id);
}
catch (LiveConnectException ex)
{
if (ex.HResult == -2146233088)
{
Debug.WriteLine("The folder already existed.");
}
else
{
Debug.WriteLine("Exception during folder creation: {0}", ex.Message);
}
}
catch (Exception ex)
{
// Get the code monkey's attention.
Debugger.Break();
}
}
The app needs to remember the id of the folder, because it is needed in the further calls. Therefore we store it in the roaming settings:
ApplicationDataContainer settings = ApplicationData.Current.RoamingSettings;
public string FolderId
{
get { return this.settings.Values["FolderId"].ToString(); }
private set { this.settings.Values["FolderId"] = value; }
}
In case you lose the folder id, you can fetch it with a LiveConnectClient.GetAsync call:
private async Task FetchfolderId()
{
LiveOperationResult lor = await client.GetAsync("me/skydrive/files");
dynamic result = lor.Result;
this.FolderId = string.Empty;
foreach (dynamic file in result.data)
{
if (file.type == "folder" && file.name == this.FolderName)
{
this.FolderId = file.id;
}
}
}
You can add files to the working folder with LiveConnectClient.BackGroundUploadAsync:
private async Task SaveAsFile(string content, string fileName)
{
// String to UTF-8 Array
byte[] byteArray = Encoding.UTF8.GetBytes(content);
// Array to Stream
MemoryStream stream = new MemoryStream(byteArray);
// Managed Stream to Store Stream to File
await client.BackgroundUploadAsync(
this.FolderId,
fileName,
stream.AsInputStream(),
OverwriteOption.Overwrite);
}
.Net developers would be tempted to apply Unicode encoding. Just hold your horses and stick to UTF-8. To convince you that it covers your needs, I added some French (containing accents and other decorations) and Chinese (containing whatever the Bing translator gave me) texts in the source code.
After clicking the ‘Save Files’ button in the sample app, the folder and its content become visible in the user’s File Explorer:
Using LiveConnectClient.GetAsync you can read the folder’s content to enumerate the list of files in it:
private async void OpenFolder_Executed()
{
try
{
// Don't forget '/files' at the end.
LiveOperationResult lor = await client.GetAsync(this.FolderId + @"/files");
dynamic result = lor.Result;
this.files.Clear();
foreach (dynamic file in result.data)
{
if (file.type == "file")
{
string name = file.name;
string id = file.id;
this.files.Add(new OneDriveFile() { Name = name, Id = id });
Debug.WriteLine("Detected a file with name '{0}' and id '{1}'.", name, id);
}
}
}
catch (LiveConnectException ex)
{
Debug.WriteLine("Exception during folder opening: {0}", ex.Message);
}
catch (Exception ex)
{
// Get the code monkey's attention.
Debugger.Break();
}
}
The OneDriveFile class in this code snippet does not come from the API, but is just a lightweight custom class. My sample app is only interested in the name and id of each file, but the API has a lot more to offer:
/// <summary>
/// Represents a File on my OneDrive.
/// </summary>
public class OneDriveFile
{
public string Name { get; set; }
public string Id { get; set; }
}
After that OpenFolder call, the sample app displays the list of files:
With a file’s id, we can fetch its content through a LiveConnectClient.BackgroundDownloadAsync call:
private async void ReadFile_Executed()
{
if (this.selectedFile != null)
{
// Don't forget '/content' at the end.
LiveDownloadOperationResult ldor = await client.BackgroundDownloadAsync(this.selectedFile.Id + @"/content");
// Store Stream to Managed Stream.
var stream = ldor.Stream.AsStreamForRead(0);
StreamReader reader = new StreamReader(stream);
// Stream to UTF-8 string.
this.SelectedText = reader.ReadToEnd();
}
}
Here’s the result in the sample app:
For the sake of completeness: LiveConnectClient also hosts asynchronous methods to move, copy, and delete files and folders.
This solution has many advantages:
- You can store more data that the app’s roaming folder can handle.
- The data is accessible across devices.
- The end user is not confronted with logon screens or file pickers.
- You don’t have to provide Cloud storage of your own.
There are some drawbacks too:
- You’ll have to deal with the occasional latency, especially when uploading files to OneDrive.
- If the client is not always connected, you might need a fallback mechanism to local storage (which uses a different file API).
Here’s the full source code of the sample app, it was created with Visual Studio 2013 for Windows 8.1. I cleared the app store association, so you’ll have to hook it to your own account: U2UC.WinRT.OneDriveSample.zip (5.1MB).
Enjoy!
Diederik