November 2011 - Posts

Working at an International company now, I found that I was often running into issues looking at the time stamps of documents.  Our collaboration server was in EST while I was in CST.  It could be confusing at times when you were working on a document for a while and you weren’t sure if the latest version had been saved.  While I was at #SPC11, I sat in a session and picked up this quick tip.  You can actually configure SharePoint to display times in your own time zone.  If you’ve been working for a large company, maybe you already knew this, but I had never noticed it before.

It’s quite simple to configure.  In the top-right menu (the one with your name on it), there is a menu item labeled My Settings.

MySettingsMenuItem

Clicking this link will bring you to a page that contains profile information.

MyProfile

However, we’re not interested in the profile, we’re interested in the My Regional Settings button at the top.  Click it and we can fix the problem. 

RegionalSettingsDefault

Uncheck the Always follow web settings checkbox and then we can change our time zone.

MySettingsModified

This is also where you can change the Locale as well if you want your dates in a different format.  Once you make these changes, your document libraries will show your documents with time stamps adjusted for your time zone.  This setting is available in SharePoint 2010, SharePoint 2007, as well SharePoint Online.  The screenshots were from my Office 365 account.

I had the honor of speaking at SharePoint Saturday Denver this weekend.  Clayton Cobb (@warrtalon) and the people from Denver did a great job putting together this event.  I spoke about how to improve your SharePoint Search experience.  I’ve uploaded my slides to SlideShare at the link below.  Denver was fun, but I have learned this weekend that the mountains aren’t for me. :)  This was a great event and certainly one I will remember.

Making the most of SharePoint Search

The code for the advanced search application I demoed can be found in this post.  The customization you can make to add a View Folder link to your search results, can be found in this post.

Follow me on twitter.

I saw this question in the forums the other day, so I decided to see if it is possible to use SharePoint search to query just the contents of a document set.  It turns out that you can and it is quite easy.  Consider the following document set.  It has a number of contracts in it.

image

I want to be able to search the content of the document set.  Document Sets are really just fancy folders.  You can tell this by examining the URL when you are viewing one.  Since it is just a folder, I realized that we can make use of the Site keyword that I have talked about before.  Simply provide the URL and you will return all of the results of the document set.  Here is an example.  I can return the entire contents of the document set like in this example.

site:"http://sp2010/RM Test/Work Package 1/"

In the above example, my server is sp2010, the document library is RM Test and the document set is named Work Package 1.  Here are the search results.  Note, that it returns the documents in the document set as well as the welcome page.

DocumentSetSearchResultsFull

Of course we can combine this with other terms.  For example if I want to search for documents in this set that involve the state of Texas, I would issue the following query.

site:"http://sp2010/RM Test/Work Package 1/" Texas

DocumentSetSearchResultsTexas

As you can see querying the document set is easy.  Now your users might not be able to create a query like that very easily, but as a developer it gives you something to work with.  To search within large document sets, you could add a custom SearchBoxEx web part to the welcome page.  The reason you would have to customize it is because you need the site keyword to pass in the URL of the document set.  This could be done by inheriting from SearchBoxEx and by adding some simple code to get the current URL.  I’ll look at building this web part in the future when I have more time.  In the meantime, if you build one, drop me a line and tell me about it.

I’m on my way to Denver this week and I’m really excited.  I’m speaking at SharePoint Saturday Denver.  This year’s event is a two day event (11/11 and 11/12).  I’ll be talking about one of my favorite topics SharePoint Search.  If you are going to be in town, be sure and stop by my session at 4:00 on Friday where we’ll discuss how to make search better in your SharePoint environment.  I can’t wait!

Getting the most out of SharePoint Search

At SharePoint Conference 2011, I showed off a great looking advanced search application using Silverlight 4.  This application queried the Search web service at /_vti_bin/search.asmx to retrieve results and display them directly inside the application.  A couple of years ago, I demonstrated how to build an advanced search application with Silverlight 3.  This application is very similar to that one except that I take it a step further and show you more of the possibilities of what the user interface could look like. 

The code you will see today was intended for Office 365 / SharePoint Online but will work quite well with SharePoint 2010 (and to a degree SharePoint 2007).  Everything from the pervious article pretty much applies.  We create a reference to search.asmx, we build an XML input document, and then we make an asynchronous call to the web service.  One thing I will point out is that I have been unsuccessful in getting the ClientAccessPolicy.xml file to work with SharePoint Online.  This means that the application cannot run locally to allow us to debug it.  I’ve posted to the Office 365 forums but have had no luck.  If anyone figures this out, please let me know.

The way we build the input XML document and call the web service is exactly the same as the pervious post.  However, what is different is the actual keyword query we construct.  Let’s take a look at what the interface looks like first.

image

There is a number of things going on here in this interface.  We first provide the user to do a simple keyword query search.  However, we also give the user the ability to query by File Size, Modified Date, Author, and by Document Only.  To do this, we use the following built-in managed properties respectively FileSize, Write, Author, and IsDocument.  The user can select any combination of the above to get a more specific query.  When the user clicks the SearchButton, our code builds a custom keyword query and sends it to the search web service.  The QueryTextBox displays the query that was constructed by the code.  However, it can also be modified by the user to test out a query manually.  This serves as a great search query testing tool.  After the user searches, the returned XML document is displayed in the large multiline textbox.  Beneath the textbox, I have added a Telerik GridView control.  I had this available to me so I decided to use it.  I think you could just as well have used a DataGrid control to bind the data too.

The code for the Silverlight application is surprisingly simple.  When the user clicks the SearchButton, we begin to construct the keyword query we want to pass to the web service.  To do this, we need a StringBuilder class so be sure and add a reference to System.Text.  We then check each control to see if it has a value.  For example, for SearchTextBox, if it has a value we simply append it to the StringBuilder named searchQuery.

if (!string.IsNullOrEmpty(SearchTextBox.Text))
    searchQuery.AppendFormat("{0} ", SearchTextBox.Text);

The FileSize managed property has an operator with values such as >, >=, < and, <=.  These are contained in the ModifiedDateOperatorComboBox.  If there is a value then we append it to searchQuery.

if (FileSizeOperatorComboBox.SelectedItem != null)
    searchQuery.AppendFormat("Size{0}\"{1}\" ",
        ((ComboBoxItem)FileSizeOperatorComboBox.SelectedItem).Content.ToString(),
        FileSizeTextBox.Text);

We continue to this for the rest of the controls on the page in the SearchButton click event handling method.  Here is the entire method.

private void SearchButton_Click(object sender, RoutedEventArgs e)
{
    StringBuilder searchQuery = new StringBuilder();

    if (!string.IsNullOrEmpty(SearchTextBox.Text))
        searchQuery.AppendFormat("{0} ", SearchTextBox.Text);

    if (FileSizeOperatorComboBox.SelectedItem != null)
        searchQuery.AppendFormat("Size{0}\"{1}\" ",
            ((ComboBoxItem)FileSizeOperatorComboBox.SelectedItem).Content.ToString(),
            FileSizeTextBox.Text);

    if (ModifiedDateOperatorComboBox.SelectedItem != null)
        searchQuery.AppendFormat("Write{0}\"{1}\" ",
            ((ComboBoxItem)ModifiedDateOperatorComboBox.SelectedItem).Content.ToString(),
            ModifiedDatePicker.SelectedDate.ToString());

    if (!string.IsNullOrEmpty(AuthorTextBox.Text))
        searchQuery.AppendFormat("Author:\"{0}\" ", AuthorTextBox.Text);

    if (DocumentsOnlyCheckBox.IsChecked.Value)
        searchQuery.Append("IsDocument:1 ");

    // pass the search query to the method to actually call the search service
    QuerySearchService(searchQuery.ToString());
}

The QuerySearchService method makes the actual call to the web service.  Since we’re dealing with Silvelright, we have to call the web service method asynchronously.  We do this by binding an event handling method to the QueryExCompleted event.  Again for more details on how the XML is constructed see my information from the Silverlight 3 post.

private void QuerySearchService(string searchQuery)
{
    QueryServiceSoapClient queryService = new QueryServiceSoapClient();
    queryService.QueryExCompleted += new EventHandler<QueryExCompletedEventArgs>(QueryService_QueryExCompleted);

    QueryTextBox.Text = searchQuery;
    StringBuilder queryXml = new StringBuilder();

    queryXml.Append("<QueryPacket xmlns=\"urn:Microsoft.Search.Query\" Revision=\"1000\">");
    queryXml.Append("<Query domain=\"QDomain\">");
    queryXml.Append("<SupportedFormats>");
    queryXml.Append("<Format>");
    queryXml.Append("urn:Microsoft.Search.Response.Document.Document");
    queryXml.Append("</Format>");
    queryXml.Append("</SupportedFormats>");
    queryXml.Append("<Range>");
    queryXml.Append("<Count>50</Count>");
    queryXml.Append("</Range>");
    queryXml.Append("<Context>");
    queryXml.Append("<QueryText language=\"en-US\" type=\"STRING\">");
    queryXml.Append(searchQuery);
    queryXml.Append("</QueryText>");
    queryXml.Append("</Context>");
    queryXml.Append("</Query>");
    queryXml.Append("</QueryPacket>");

    BusyIndicator.IsBusy = true;
    queryService.QueryExAsync(queryXml.ToString());
}

The last line passed the XML input document to the web service method.  Now, it’s just a matter of handling the return results in the event handling method.  The first thing we need to do is get the XML document with the results.  We can always find this in the Result.Nodes[1] object available in QueryExCompletedEventArgs.  For convenience, I write this value to a TextBox so that I can view it. 

ResultsTextBox.Text = e.Result.Nodes[1].ToString();

However, I want to bind this XML to our nice looking RadGridView.  To do this I must extract the data from the XDocument and expose it in a custom type. Here is where the LINQ to XML comes in.  Normally, I would just use an anonymous type for this, but that doesn’t work in Silverlight.  This means I have to create a new class to hold our search results.  I call this class SearchResult.

public class SearchResult
{
    public string Title { get; set; }
    public string Path { get; set; }
    public string Author { get; set; }
    public string Size { get; set; }
    public DateTime? Write { get; set; }
    public string SiteName { get; set; }
    public string HitHighlightedSummary { get; set; }
    public string ContentClass { get; set; }
    public bool IsDocument { get; set; }
}

I then use LINQ to XML to write the value of each property in.  Since nulls are a real possibility, I use .Any() before assigning each value to ensure we don’t get an exception.  To understand the LINQ we use, let’s take a quick look at the result XML document.

<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
    <Results xmlns="">
        <RelevantResults diffgr:id="RelevantResults1" msdata:rowOrder="0">
            <WorkId>2799582</WorkId>
            <Rank>78969610</Rank>
            <Title>Sales</Title>
            <Size>65211</Size>
            <Path>https://dotnetmafia.sharepoint.com/sites/fabrikam/teamsites/sales</Path>
            <Write>2011-08-11T07:11:59-07:00</Write>
            <SiteName>https://dotnetmafia.sharepoint.com/sites/fabrikam/teamsites</SiteName>
            <CollapsingStatus>0</CollapsingStatus>
            <HitHighlightedSummary>Site Actions &lt;ddd/&gt; This page location is: &lt;ddd/&gt; Home &lt;ddd/&gt; Team Sites &lt;ddd/&gt; Pages &lt;ddd/&gt; default &lt;ddd/&gt; Employee &lt;ddd/&gt; Resources &lt;ddd/&gt; Facilities &lt;ddd/&gt; News &lt;ddd/&gt; I Like It &lt;ddd/&gt; Tags &amp;amp; Notes &lt;ddd/&gt; Libraries &lt;ddd/&gt; Shared Documents &lt;ddd/&gt; Flyers &lt;ddd/&gt; Presentations &lt;ddd/&gt; Proposals &lt;ddd/&gt; &lt;c0&gt;Sales&lt;/c0&gt; Forecasts &lt;ddd/&gt; Lists &lt;ddd/&gt; Calendar &lt;ddd/&gt; Tasks &lt;ddd/&gt; </HitHighlightedSummary>
            <HitHighlightedProperties>&lt;HHTitle&gt;&lt;c0&gt;Sales&lt;/c0&gt;&lt;/HHTitle&gt;&lt;HHUrl&gt;https://dotnetmafia.sharepoint.com/sites/fabrikam/teamsites/&lt;c0&gt;sales&lt;/c0&gt;&lt;/HHUrl&gt;</HitHighlightedProperties>
            <ContentClass>STS_Web</ContentClass>
            <IsDocument>false</IsDocument>
        </RelevantResults>

Each search result is contained inside a ReleventResults node inside of the Results element.  So we look inside there to create our query.

var results = from result in
                                  e.Result.Nodes[1].Descendants("RelevantResults")
                              select new SearchResult
                              {
                                  Title = (result.Elements("Title").Any())
                                    ? result.Element("Title").Value : string.Empty,
                                  Path = (result.Elements("Path").Any())
                                    ? result.Element("Path").Value : string.Empty,
                                  Author = (result.Elements("Author").Any())
                                    ? result.Element("Author").Value : string.Empty,
                                  Size = (result.Elements("Size").Any())
                                    ? result.Element("Size").Value : string.Empty,
                                  Write = (result.Elements("Write").Any())
                                    ? DateTime.Parse(result.Element("Write").Value) : DateTime.MinValue,
                                  SiteName = (result.Elements("SiteName").Any())
                                    ? result.Element("SiteName").Value : string.Empty,
                                  HitHighlightedSummary = (result.Elements("HitHighlightedSummary").Any())
                                    ? result.Element("HitHighlightedSummary").Value : string.Empty,
                                  ContentClass = (result.Elements("ContentClass").Any())
                                    ? result.Element("ContentClass").Value : string.Empty,
                                  IsDocument = (result.Elements("IsDocument").Any())
                                    ? bool.Parse(result.Element("IsDocument").Value) : false
                              };

We simply assign each property after verifying that it’s not null.  Most values are strings but we did do some casting for DateTime and Boolean values.  The last thing we do is bind to the RadGridView.

ResultsRadGridView.ItemsSource = results;

The next section applies to the Telerik specific content.  If you don’t have those controls available to you, you can skip this section and you can configure the built-in grid in a similar manner.  RadGridView has some column types that allow us to format links and checkboxes in a nice manner.  Telerik has free trials available if you are interested.  Here is what that code looks like.

<telerik:RadGridView HorizontalAlignment="Left" Margin="13,227,0,0" Name="ResultsRadGridView" VerticalAlignment="Top" Width="776" AutoGenerateColumns="False">
    <telerik:RadGridView.Columns>
        <telerik:GridViewDynamicHyperlinkColumn Header="Title" DataMemberBinding="{Binding Title}" NavigateUrlFormatString="{} {0}" NavigateUrlMemberPaths="Path" TargetName="_blank" />
        <telerik:GridViewDataColumn Header="Author" DataMemberBinding="{Binding Author}" />
        <telerik:GridViewDataColumn Header="Write" DataMemberBinding="{Binding Write}" DataFormatString="{} {0:MMM, dd, yyyy}" />
        <telerik:GridViewDataColumn Header="Size" DataMemberBinding="{Binding Size}" />
        <telerik:GridViewDataColumn Header="HitHighlightedSummary" DataMemberBinding="{Binding HitHighlightedSummary}" />
        <telerik:GridViewDataColumn Header="ContentClass" DataMemberBinding="{Binding ContentClass}" />
        <telerik:GridViewHyperlinkColumn Header="SiteName" DataMemberBinding="{Binding SiteName}" TargetName="_blank" />
        <telerik:GridViewCheckBoxColumn Header="IsDocument" DataMemberBinding="{Binding IsDocument}" />
    </telerik:RadGridView.Columns>
</telerik:RadGridView>

That’s all the code that is involved.   Here is a screenshot of it in action.

image

I’ve attached the code to this post (minus the Telerik controls). This code will work on-premises or in the cloud with SharePoint Online.  I’ve confirmed this works with both the P1 and E3 SKUs of Office 365. I just used built-in managed properties in my example, but if you create custom properties of your own you can add those as well. Try it out and see what you think.