Blank Open/Save Dialog When Browsing Document Library From Office Clients

I’ve found that when you browse to a document library from an office client dialog such as ‘MS Word > Open’, the dialog shows a blank space instead of the document library contents.
Page Content

 

Problem

When you browse to a document library from an office client dialog such as ‘MS Word > Open’, the dialog shows a blank space instead of the document library contents-

2011-09-30-BlankOpenSaveDialog-01.png

Root Cause

This issue is caused by the lack of an appropriate ‘FormDialog’ view available in the document library schema. Any document library definitions created by Visual Studio 2010 suffer from this issue. If you notice, these DocLib schemas provisioned by VS do actually contain a stub for a file dialog view but this stub is not sufficient in and of itself for proper rendering within an office client dialog-

1 <View BaseViewID="2" Type="HTML" FileDialog="TRUE" TabularView="FALSE" DisplayName="$Resources:core,File_Dialog_View;" Hidden="TRUE" Path="filedlg.htm" ModerationType="Moderator">

Solution

If you are in fact working with a visual studio provisioned schema for a document library (most common scenario for this issue) then simply find the FileDialog view (BaseViewId=”2” as shown above) and make the following modifications to the view-

    1. Remove the toolbar
1 <Toolbar Type="Standard" />
    1. Add ViewHeader
01 <ViewHeader>
02   <SetVar Name="FileDialog">1</SetVar>
03   <HTML>
04     <![CDATA[
05     <table id="FileDialogViewTable" width="100%" style="cursor: default;" border="0" rules="rows" cellspacing="0" cellpadding="2">
06 <tr>
07 ]]>
08   </HTML>
09   <Fields>
10     <HTML><![CDATA[<th class="ms-vh2-nofilter">]]></HTML>
11     <Field />
12     <HTML><![CDATA[</th>]]></HTML>
13   </Fields>
14   <HTML><![CDATA[</tr>]]></HTML>
15 </ViewHeader>
    1. Add ViewBody
01 <ViewBody>
02   <SetVar Name="FileDialog">1</SetVar>
03   <IfEqual>
04     <Expr1>
05       <GetVar Name="AlternateStyle" />
06     </Expr1>
07     <Expr2>ms-alternating</Expr2>
08     <Then>
09       <SetVar Scope="Request" Name="AlternateStyle">
10       </SetVar>
11     </Then>
12     <Else>
13       <SetVar Scope="Request" Name="AlternateStyle">ms-alternating</SetVar>
14     </Else>
15   </IfEqual>
16   <Switch>
17     <Expr>
18       <LookupColumn Name="FSObjType" />
19     </Expr>
20     <Case Value="1">
21       <HTML><TR fileattribute=folder ID="</HTML>
22     </Case>
23     <Default>
24       <HTML><TR fileattribute=file ID="</HTML>
25     </Default>
26   </Switch>
27   <Field Name="EncodedAbsUrl" />
28   <HTML><![CDATA["csharp plain">]]></HTML>
29   <GetVar Name="AlternateStyle" />
30   <HTML><![CDATA[" onmousedown="selectrow()" onclick="selectrow()">]]></HTML>
31   <Fields>
32     <HTML><![CDATA[<td class="ms-vb" style="padding-left: 4px">]]></HTML>
33     <FieldSwitch>
34       <Expr>
35 <Property Select="Type" />
36       </Expr>
37       <Case Value="User">
38 <LookupColumn HTMLEncode="TRUE" />
39       </Case>
40       <Default>
41 <FieldSwitch>
42   <Expr>
43     <Property Select="Name" />
44   </Expr>
45   <Case Value="CheckoutUser">
46     <Field HTMLEncode="TRUE" />
47   </Case>
48   <Default>
49     <Field />
50   </Default>
51 </FieldSwitch>
52       </Default>
53     </FieldSwitch>
54     <HTML><![CDATA[</td>]]></HTML>
55   </Fields>
56   <HTML><![CDATA[</tr>]]></HTML>
57 </ViewBody>
    1. Add ViewFooter
1 <ViewFooter>
2   <HTML><![CDATA[</table>]]></HTML>
3 </ViewFooter>
    1. Add ViewEmpty
01 <ViewEmpty>
02   <SetVar Name="FileDialog">1</SetVar>
03   <HTML>
04     <![CDATA[
05     <table id="FileDialogViewTable" width="100%" style="cursor: default;" border="0" rules="rows" cellspacing="0" cellpadding="2">
06 <tr>
07 ]]>
08   </HTML>
09   <Fields>
10     <Switch>
11       <Expr>
12 <Property Select="Name" />
13       </Expr>
14       <Case Value="FileLeafRef">
15       </Case>
16       <Default>
17 <HTML><![CDATA[<th class="ms-vh2-nofilter">]]></HTML>
18 <Field />
19 <HTML><![CDATA[</th>]]></HTML>
20       </Default>
21     </Switch>
22   </Fields>
23   <HTML><![CDATA[</tr></table>]]></HTML>
24   <HTML><![CDATA[<table width="100%" border="0" rules="rows"><tr>]]></HTML>
25   <HTML><![CDATA[<td class="ms-vb">]]></HTML>
26   <HTML>$Resources:core,noDocOfSpecType;</HTML>
27   <HTML><![CDATA[</td></tr></table>]]></HTML>
28 </ViewEmpty>

Things to note

    1. If you’re creating your own list schema by hand, make sure there is a view definition with the following attributes-
1 <View BaseViewID="" Type="HTML" FileDialog="TRUE" TabularView="FALSE" DisplayName="" Hidden="TRUE" Path="filedlg.htm" ModerationType="Moderator">
  1. For lists that have already been provisioned using a custom schema, simply update the schema definition and redeploy to enact the change in existing list instances. Be sure to re-activate the feature so that an updated schema is read from the disk.
Advertisements

Building and consuming a custom oData service for SharePoint 2010

The advantage of oData is its ability to build rich RESTFUL Urls and also to be able to consume it from a server-side control just like we do with ListData.svc.
Page Content

For a recent project, I’ve been involved with a fellow SharePoint developer Jeroen Van Bastelaere (http://www.skavi.be) in optimizing a SharePoint portal application.

The first idea we had was to use caching, so we built a lightweight caching framework that is flexible and maintainable enough and that allowed us to reach our main objective which was gaining performance.

I tell this little story because the topic of this post is actually inherited from the above context. Thus, still having the performance objective in mind, we’ve also been asked to build asynchroneous controls (webparts, user controls…) for user friendlyness and fast initial page loads.

Of course, one could have chosen a common approach which is to make use of jQuery together with ListData.svc/Lists.asmx or involve the ECMAScript COM and that might have been ok but…I prefered to consume our data from our caching system to make sure this would be the most performant approach (server side at least).

At first, I thought of simply writing a REST service using the built-in SharePoint MultipleBaseAddressWebServiceHostFactory . That could have paid the bill but I wanted something very flexible in terms of query engine so I ended up building a custom oData service that consumes our cached data.

One of the main cached objects we dealt with is User Profiles. For scalability and performance reasons, we decided to get all the user profiles via a search query, cache them during the entire day since they get synchronized only once a day, and then, target this cache for any subsequent User Profile query. Our business scenario allowed us to cache them for an entire day.

That works very well and also prevents the system from overloading the User Profiles store with thousands of connections. This is, threfore, not only more performant but also more robust.

That said, the example I will show you in this post demonstrates how to consume that User Profile cache with a custom oData service.

The advantage of oData is its ability to build rich RESTFUL Urls and also to be able to consume it from a server-side control just like we do with ListData.svc.

So, I’ll cover the following topics in this article:

  1. Building a the cache framework (basic subset of what we’ve done to avoid unnecessary complexity)
  2. Building a CachedData.svc service that will for this example expose oData operations on the cached User Profiles (this could of course be extended to any kind of data)
  3. Consume it with jQuery
  4. Consume it from a Console Application

Pre-requisites

ADO.NET Data Services must be installed on the system, you can download it from there : http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=2343

Creating the project

  1. Create an empty SharePoint 2010 Project
  2. Add the following references :
    Microsoft.SharePoint.Client.ServerRuntime.dll Microsoft.Office.Server.Search.dll System.Data.Services System.Data.Services.Client.dll System.Runtime.Serialization System.ServiceModel.dll System.ServiceModel.Web.dll Microsoft.Office.Server.dll System.Web.dll System.Web.Services.dll

Still there?2011-11-04-CustomODataService-07.jpg

Building the cache framework

As said before, this light framework enables storing data in cache very easily. It’s made of a few classes, I’ve removed most of its complexity in order to have something as understandable as possible since this is not the main topic of this post.

I’ve also hard-coded some values and removed exception handling for the sake of simplicity.

First, the interface that defines what any cached member should implement:

1 public interface ISharePointData<T>
2     {
3         List<T> GetItems();              
4         int CacheDurationInSeconds { get; set; }
5         int CacheDurationInMinutes { get; set; }
6         int CacheDurationInHours { get; set; }        
7     }

Here, the contract specifies that any member should implement a GetItems() method and the time to be set in cache before expiration.

Now, a base class that implements our interface and specifies some defaults. In this case (cleaned out), its usage is less interesting than in the real implementation…

01 public abstract class BaseSharePointData<T> : ISharePointData<T>
02     {
03         public abstract List<T> GetItems();              
04         private int _cacheDuration = 0;
05         public int CacheDurationInSeconds
06         {
07             get
08             {
09                 return _cacheDuration;
10             }
11             set
12             {
13                 _cacheDuration = value;
14             }
15         }
16         public int CacheDurationInMinutes
17         {
18             get
19             {
20                 return CacheDurationInSeconds / 60;
21             }
22             set
23             {
24                 CacheDurationInSeconds = value * 60;
25             }
26         }
27         public int CacheDurationInHours
28         {
29             get
30             {
31                 return CacheDurationInMinutes / 60;
32             }
33             set
34             {
35                 CacheDurationInMinutes = value * 60;
36             }
37         }
38         
39     }

At last, the definition of a member, in this case, our User Profile object:

01 [Serializable()]
02 [DataContractAttribute(IsReference = true)]
03 [DataServiceKey("UserName")]
04 public class UserProfileObject : BaseSharePointData<UserProfileObject>
05 {        
06     public UserProfileObject()
07     {                        
08         CacheDurationInMinutes = 10;
09     }
10     public UserProfileObject(string userName, string email, string pictureURL, string path, string jobTitle)
11         : base()
12     {
13         UserName = userName;            
14         PictureURL = pictureURL;
15         Email = email;
16         Path = path;
17         JobTitle = jobTitle;
18     }
19         
20         
21     [DataMemberAttribute()]
22     public string UserName { get; set; }
23     [DataMemberAttribute()]
24     public string FirstName { get; set; }
25     [DataMemberAttribute()]
26     public string LastName { get; set; }             
27     [DataMemberAttribute()]
28     public string Email { get; set; }
29     [DataMemberAttribute()]
30     public string PictureURL { get; set; }
31     [DataMemberAttribute()]
32     public string Path { get; set; }
33     [DataMemberAttribute()]
34     public string JobTitle { get; set; }
35
36     public override List<UserProfileObject> GetItems()
37     {
38         List<UserProfileObject> allProfiles = new List<UserProfileObject>();
39         FullTextSqlQuery q = new FullTextSqlQuery(ServerContext.Current);
40         q.QueryText = "SELECT AccountName,FirstName,LastName, WorkEmail,PictureUrl,Path,JobTitle FROM Scope() WHERE \"Scope\"='People'";
41         q.ResultTypes = ResultType.RelevantResults;
42         q.RowLimit = 20000;
43         ResultTableCollection results = q.Execute();
44         ResultTable queryResultsTable = results[ResultType.RelevantResults];
45         DataTable queryDataTable = new DataTable();
46         queryDataTable.Load(queryResultsTable, LoadOption.OverwriteChanges);
47         AddProfiles(allProfiles, queryDataTable.Rows);                        
48         return allProfiles;
49     }
50
51     public void AddProfiles(List<UserProfileObject> profiles, DataRowCollection rows)
52     {            
53         for (int i = 0; i < rows.Count; i++)
54         {
55
56             DataRow row = rows[i];
57             profiles.Add(new UserProfileObject
58             {         
59                     
60                 UserName = (row["AccountName"] != null) ? row["AccountName"].ToString() : string.Empty,                    
61                 FirstName = (row["FirstName"] != null) ? row["FirstName"].ToString() : string.Empty,
62                 LastName = (row["LastName"] != null) ? row["LastName"].ToString() : string.Empty,
63                 Email = (row["WorkEmail"] != null) ? row["WorkEmail"].ToString() : string.Empty,
64                 PictureURL = (row["PictureURL"] != null) ? row["PictureURL"].ToString() : string.Empty,
65                 JobTitle = (row["JobTitle"] != null) ? row["JobTitle"].ToString() : string.Empty,
66                 Path = (row["Path"] != null) ? row["Path"].ToString() : string.Empty                    
67             });
68         }
69     }
70         
71 }

This one needs some extra comments:2011-11-04-CustomODataService-07.jpg

  1. 1st, we already specified that the primary key of this object is the UserName property, that is required by oData.
  2. 2nd, we implement the GetItems() method since it is required by our Interface. This method performs the search query taking all the profiles from the People scope with the properties we want. In this case, I only take a subset of them.
  3. Then, we create the helper method AddProfiles() that builds our list of UserProfileObject.

The expected behavior is that when called for the first time, the code will perform the search query and then, it will just consume its data from the cache.

Building the service

Adding the .svc file

As with every WCF service, you’ll need to create a .SVC file.

  1. Create a SharePoint mapped folder onto ISAPI
  2. Create an empty text file with the extension .SVC and add it the following:
1 <%@ ServiceHost 
2     Language="C#"
3     Debug="true"
4     Factory="Microsoft.SharePoint.Client.Services.MultipleBaseAddressDataServiceHostFactory, Microsoft.SharePoint.Client.ServerRuntime, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
5     Service="demoOdata.Services.CacheoDataService,$SharePoint.Project.AssemblyFullName$"
6 %>

The most important thing here is that we tell SharePoint to use the MultipleBaseAddressDataServiceHostFactory factory. This is the one that allows us to build ADO.Net Data Services and to deploy them into SharePoint.

Regarding the service deployment into SharePoint, that’s about all you have to do, not a big effort as you can see.

Adding the services classes

Now that we’ve created the .SVC file, we need to build the code which is failry simple as you will see.

The service class looks like this:

01 [BasicHttpBindingServiceMetadataExchangeEndpoint]
02 [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
03 [System.ServiceModel.ServiceBehavior(
04 IncludeExceptionDetailInFaults = true)]
05 public class CacheoDataService : DataService<CacheDataContext>
06 {
07     public static void InitializeService(DataServiceConfiguration config)
08     {
09         config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);            
10         config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
11     }
12 }

The first attribute is used to expose a WCF MEX. The 2nd attribute is required by SharePoint to make the service ASP.NET friendly.

At last, the 3rd attribute is used to display the error in the response body in case something goes wrong. During the development phase, it’s useful.2011-11-04-CustomODataService-07.jpg

Then, I specify that all the operations are accessible. In this case, I have only one operation and I don’t handle CRUD queries. I only implement the R from CRUD.

As you can see, our service inherits from the DataService<T> class which allows specifying another type of object, in this case our custom CacheDataContext.

01 public class CacheDataContext
02     {
03         DataHelper dataHelper = new DataHelper();
04
05         public IQueryable<UserProfileObject> UserProfiles
06         {
07             get
08             {
09                 return dataHelper.GetItems<UserProfileObject>().AsQueryable();
10             }
11         }
12     }

The only important thing to remember here is that you need to return a IQueryable<T> or another object that implements IQueryable. In this case, I make use of the cache framework to retrieve the list of user profile objects.

If I wanted to add another operation, I could just write it this way:

1 public IQueryable<News> News
2 {
3     get
4     {
5         return dataHelper.GetItems<News>().AsQueryable();
6     }
7 }

Which would allow me to return news from the Cache.

If I had worked with a database as a datasource, I’d have most probably used Entity Framework but here, the data simply comes from memory so I can just return it like that.

So, the overall solution should look like this :

2011-11-04-CustomODataService-01.png

Note that you’ll have more info on the odataTestPage.htm later in this article.

Deploying the solution and checking if the service is working

By just typing the URL to the endpoint, you should get all the available operations, in our example, there is only one but you could have more of course. Do not forget to end the URL with the “/” character.

2011-11-04-CustomODataService-02.png

Now, if you just add the operation to the URL, you’ll get all the user profiles:

2011-11-04-CustomODataService-03.png

And you can start using oData querystring params such as $filter, $select etc…which makes your query engine much more PowerFull than a “simple” custom RESTFUL service where you’d have to handle those params yourself.

Consuming the service from a console application

    1. Just create a Console application
    2. Add a service reference and use the URL to the endpoint

2011-11-04-CustomODataService-04.png

  1. Add for instance the following piece of code:
1 CacheDataContext.CacheDataContext ctx = new CacheDataContext.CacheDataContext(
3 ctx.Credentials = System.Net.CredentialCache.DefaultCredentials;
4 var query = from profile in ctx.UserProfiles
5             where profile.JobTitle != string.Empty
6             select profile;
7 foreach (var queryresut in query)
8     Console.WriteLine(queryresut.UserName);

This will output:

2011-11-04-CustomODataService-05.png

Consuming the service with jQuery

To test that, you can just add a testpage.html into your /_layouts/ folder and include the following piece of code:

01 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
02 <html>
03     <head>
04         <title></title>
05         <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
06         <script type="text/javascript">
07             $(document).ready(function () {
08                 $.ajax({
09                     url: "_vti_bin/CachedoData.svc/UserProfiles()",
10                     dataType: "json",
11                     success: function (data) {
12                         if (data.d) {
13                             var html = "";
14                             for (var i = 0; i < data.d.length; i++) {
15
16                                 html += data.d[i].UserName + "<br/>";
17
18                             }
19                             $("#response").empty().append(html);
20                         }
21                     },
22                     error: function (XMLHttpRequest, textStatus) {
23                         alert(XMLHttpRequest.status);
24                     }
25                 });
26             });          
27         </script>
28     </head>
29     <body>
30     <div id="response"></div>
31     </body>
32 </html>

This will just output all the usernames :

2011-11-04-CustomODataService-06.png

Note that the jQuery piece of code might need to be adjusted according to the reading browser, I only tested it with IE9 & Firefox 5.0.1. You might want to read an ATOM feed in return instead of JSON…

So far, I only wrote RESTFUL services using one of the other built-in factories. I found it interesting to write a short article based on a real world scenario.