Tuesday, May 29, 2012

SharePoint 2010 Silverlight Client Object Model – How to upload a document and set metadata

Technorati Tags: ,,,

The Microsoft Client Object Model (CSOM) has three different platform libraries, one for managed code, one for Silverlight and another ECMA script based library. The client object models are provided through a proxy (.js files ECMA script) and (assembly files managed and Silverlight), which can be referenced in custom applications like other object models. The object models are implemented as a Windows Communication Foundation (WCF) service (.../_vti_bin/client.svc), but uses Web bindings to implement efficient request batching. All operations are inherently asynchronous, and commands are serialized into XML and sent to the server in a single HTTP request. The Silverlight and ECMA models require you to execute your code on a separate thread. This prevents CSOM calls locking the UI thread and making the UI unresponsive. In my opinion executing potential long running processes on a separate thread is the best approach for responsive applications. However, many developers find asynchronous programming confusing and messy. This type of programming requires callback methods to handle success and failure, thus making the flow of the code disjointed. The CSOM makes this easy for developers by providing the ClientContenxt.ExecuteQueryAsync method. This method enables you to use the Sivlerlight/ECMA object model the same way you would using the managed object model except when sending the request to the server the developer must provide a callback methods.

The two approaches to CSOM Silverlight asynchronous programming

A standard approach is to define  callback methods in your class to handle the response of the ExecuteQueryAsync method. The example below shows how to look up a user using a CAML query to the user information list. The example uses two defined methods, one for success and one for failure. The success method handles getting data back from the call. There are a couple of problems with this approach. One is that you must declare and store the return data in a class level variable and second you can not re-use the Success method because it has become tightly coupled with the class level ListItemCollection varable named users.  This approach would require you to create a class level variable for every type of return object and a success callback for each ExecuteQueryAsync call. Keeping track of which methods use which call backs can become very confusing.

public void LookupUser1()
{
    try
    {
        var clientContext = new ClientContext("http://basesmc2008/");

        CamlQuery camlQuery = new CamlQuery();
        camlQuery.ViewXml = @"<View>
                            <Query>
                                <Where>
                                    <Eq>
                                        <FieldRef Name='UserName'/>
                                        <Value Type='Text'>Steve.Curran</Value>
                                    </Eq>
                                </Where>
                            </Query>
                        </View>";

        this.users = clientContext.Web.SiteUserInfoList.GetItems(camlQuery);
        clientContext.Load(this.users, items => items.Include
                                (item => item["LastName"],
                                item => item["UserName"],
                                item => item["Title"],
                                item => item["Picture"]));

        clientContext.ExecuteQueryAsync(Success, Failure);

    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }

}

private void Success(Object sender, ClientRequestSucceededEventArgs args)
{
        try
        {
            if (this.users != null)
            {
                string title = this.users[0]["Title"].ToString();
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
}

private static void Failure(Object sender, ClientRequestFailedEventArgs args)
{
    MessageBox.Show("Failed!");
}

The second approach is to use  Anonymous methods in place of defined call back functions. Anonymous methods were introduced in C# 2.0 and the ability to use multi-line and lambda expressions was introduced in C# 3.0. http://msdn.microsoft.com/en-us/library/0yw3tz5k(v=vs.100).aspx You can use these in VB.net but they do not support multi-line and lambda expressions. The example below is the same code in the first example but now is using anonymous methods to process the return values. Anonymous methods have the ability to read and write to variables declared in the local method, so there is no need to declare them at the class level. Secondly, the code flows and remains contextually together and coherent.

public static void LookupUser2()
{
    try
    {
        var clientContext = new ClientContext("http://basesmc2008/");
        CamlQuery camlQuery = new CamlQuery();
        camlQuery.ViewXml = @"<View>
                            <Query>
                                <Where>
                                    <Eq>
                                        <FieldRef Name='UserName'/>
                                        <Value Type='Text'>Steve.Curran</Value>
                                    </Eq>
                                </Where>
                            </Query>
                        </View>";

        ListItemCollection users = clientContext.Web.SiteUserInfoList.GetItems(camlQuery);
        clientContext.Load(users, items => items.Include
                                (item => item["LastName"],
                                item => item["UserName"],
                                item => item["Title"],
                                item => item["Picture"]));        

        clientContext.ExecuteQueryAsync((object eventSender, ClientRequestSucceededEventArgs args1) =>
        {
            string title = users[0]["Title"].ToString();

        }, (object sender, ClientRequestFailedEventArgs eventArgs) =>
        {
            string message = eventArgs.Message;
        }
    );

    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }

}

 

Uploading a document and setting metadata

Some tasks in SharePoint require multiple steps and programming them with the Sivlerlight object model becomes difficult. One task would be uploading a document to a folder or document set and setting the metadata. How difficult could that be? Well this requires 4 separate steps and 4 ExecuteQueryAsync calls.

  1. Get the folder or document set
  2. Upload the file
  3. Set the metadata
  4. Update the list item

The following example shows how this can be done using anonymous methods so the task can be completed in one method block. Please note this code is not  production level code since you will have to fill code for getting the byte array of the document.

public static void UploadFile(string siteUrl,
    string listName, string targetFolder, string fileName)
{

    ClientContext context = new ClientContext(siteUrl);
    Web web = context.Web;

    Folder docSetFolder = web.GetFolderByServerRelativeUrl(listName + "/" + targetFolder);

    //STEP 1 GET FOLDER/DOCUMENTSET
    context.ExecuteQueryAsync((object eventSender, ClientRequestSucceededEventArgs eventArgs) =>
    {
        string documentUrl = "/" + listName + "/" + targetFolder + "/" + fileName;
        FileCreationInformation fci = new FileCreationInformation();
        fci.Url = documentUrl;
        fci.Content = new byte[] { }; //byte[] take your stream and convert to byte array

        FileCollection documentFiles = docSetFolder.Files;
        context.Load(documentFiles);

        //STEP 2 ADD DOCUMENT
        context.ExecuteQueryAsync((object eventSender2, ClientRequestSucceededEventArgs eventArgs2) =>
        {
            File newFile = documentFiles.Add(fci);

            context.Load(newFile);
            ListItem item = newFile.ListItemAllFields;

            //STEP 3 SET METADATA
            context.ExecuteQueryAsync((object eventSender3, ClientRequestSucceededEventArgs eventArgs3) =>
            {

                //start setting metadata here
                item["Title"] = "myimage";
                item.Update();
                context.Load(item);

                //STEP 4 UPDATE ITEM
                context.ExecuteQueryAsync((object eventSender4, ClientRequestSucceededEventArgs eventArgs4) =>
                {

                    string ret;
                    if (eventArgs.Request != null)
                        ret = eventArgs.Request.ToString();

                }, (object eventSender4, ClientRequestFailedEventArgs eventArgs4) =>
                {
                    string message = eventArgs4.Message;
                });

            }, (object eventSender3, ClientRequestFailedEventArgs eventArgs3) =>
            {
                string message = eventArgs3.Message;
            });

        }, (object eventSender2, ClientRequestFailedEventArgs eventArgs2) =>
        {
            string message = eventArgs2.Message;
        });

    }, (object eventSender, ClientRequestFailedEventArgs eventArgs) =>
    {
        string message = eventArgs.Message;
    });
}

 

Awaiting the future of asynchronous programming in Silverlight

There is no need to pull your hair out doing asynchronous programming. You can even do anonymous methods in jscript and use them with the ECMA CSOM. Anonymous methods allow you to put the code where it needs to be. The future of asynchronous programming in Silverlight can be seen now in Silverlight 5 and the System.Threading.Tasks namespace. This namespace is particularly useful with the HttpWebRequest methods that support the IAsyncResult interface. The example below shows how to use the Task.Factory to download a file and handle the response in one code block. http://msdn.microsoft.com/en-us/library/dd537609.aspx

string uri = "http://basesmc2008/lists/tester.xml";
var request = HttpWebRequest.Create(uri);
var webTask = Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null)
    .ContinueWith(task => { var response = (HttpWebResponse)task.Result;
        var stream = response.GetResponseStream();
        var reader = new StreamReader(stream);
        string xmlFileText = reader.ReadToEnd();
        });

We also await future changes to the SharePoint client object model and the possibility of using the new .Net 4.5 feature of “Async await” which hopefully will be in the next version of Silverlight. http://blogs.msdn.com/b/dotnet/archive/2012/04/03/async-in-4-5-worth-the-await.aspx