10. Accessing SharePoint with Web Services

PRIOR TO SHAREPOINT 2010, the primary way to access SharePoint data from client-side technologies such as Silverlight was to use its web services interface. Introduced in SharePoint 2003, web services provide access to SharePoint content and shared services. The primary user of these services has been the Microsoft Office client, which provides deep integration with SharePoint for document management, meeting coordination, and a variety of other collaborative tasks. They are well documented on the MSDN web site, and though some have been deprecated over the years, most are still available for use.

Web services open up the world of SharePoint shared services that span many site collections and web applications to provide core features such as enterprise search and social networking. Much of this chapter revolves around the sample SearchView Web Part that shows how to use these services from Silverlight. The sample also follows the Model View ViewModel (MVVM) pattern and has a section that explains how to use it. The SearchView web part project will show you how to access SharePoint shared services, which are only accessible using web services. You’ll learn how to use these services to search for content and people, and to provide the new Search Suggestions available in SharePoint 2010. You’ll also learn how to access SharePoint’s social networking API to read user profiles and post comments, and to read a user’s “Activity Feed”, which is an ATOM feed. At the end of the chapter, you’ll learn to develop your own WCF service to SharePoint in case you can’t find a web service that does what you need.

Web Services in SharePoint

SharePoint supports dozens of web services that offer access to nearly every part of the product. Table 10.1 shows a small subset of what’s available; for a complete list see http://bit.ly/SPSL_SPWebServices.

Table 10.1. Selected SharePoint Web Services

image

The most popular web service in Windows SharePoint Services (renamed SharePoint Foundation in 2010) was surely the Lists.asmx web service, which allows queries and updates on SharePoint lists. This functionality and much more is now available with the Client Object Model and WCF Web Services. Using traditional web services for manipulating SharePoint list content is no longer de rigueur unless access to older versions of SharePoint is necessary or if the Client Object Model and OData Services are not available from the SharePoint hosting provider.

However, with each version of SharePoint, shared services—those services that can span all content across site collections—become richer and richer. Web services are the best (and only) way to access these services. That’s why access to shared services, in particular Search and SharePoint’s social networking services, is the focus of this chapter.


Tip

The search and social networking web services used in this chapter are all part of Microsoft SharePoint Server 2010 and are not included in the “free” Microsoft SharePoint Foundation 2010 product. To run the SearchView sample solution, you also need to set up and configure both the search and user profile services.

If you downloaded the 2010 Information Worker Demonstration and Evaluation Virtual Machine mentioned in Chapter 1, these services should be up and running. If you set up your own server, ensure they are working before attempting to run the SearchView sample. Details on search configuration are available at http://bit.ly/SPSL_SearchConfig, and user profile setup is described at http://bit.ly/SPSL_ProfileConfig.

If you are developing for SharePoint Foundation, you might want to skip to the last section on building your own WCF services; however, you’d miss a lot of information on building solutions with the Model View ViewModel pattern, which is used in the SearchView Web Part sample.

The search web service is available in the “free” Search Server 2010 Express product; however, the profile service is not. If you want to develop on Search Server 2010 Express, you could adapt the solution by removing the social aspects, which are isolated to the Person Detail child window.


The SearchView Web Part Sample Solution

Most of this chapter (except for the custom WCF section) is built on a single solution called the SearchView Web Part. SearchView is able to search content or people, as shown in Figure 10.1.

Figure 10.1. SearchView Web Part with People Search results and Alpha Tabs Query

image

There are three ways to issue a search query with SearchView:

• A fixed query can be entered when editing the web part. This allows the web part to serve as a search-driven content rollup.

• The user can enter a search (and is shown Search Suggestions as they type). Optionally a fixed portion of the query can be specified when editing the web part. For example, to provide a search within a particular SharePoint site, a fixed query of “site:http://myserver/sites/mysite/” could be entered, and this term would be added to whatever the user types.

• Alphabetic tabs can be provided for selecting content beginning with a particular letter of the alphabet, again with optional fixed query terms. For example, to make a directory of the Sales department, a People search with alphabetic tabs could be augmented with a fixed query of “department:sales”.

When SearchView is configured for People search, the user can click a person in the results display for a summary of his or her user profile, activity feed, and a small text box to leave a comment for the user. This is shown in Figure 10.2.

Figure 10.2. SearchView People Detail child window

image

These social features are provided by the User Profile and Social Data web services, and the Activity Feed provides an opportunity to see how easy it is to consume an ATOM feed from SharePoint.

When the web part is edited, the editing experience is directly in the web part. Although this doesn’t really have much to do with web services, it is described in the section, “In-Place Web Part Editing Experience,” later in this chapter.

The MVVM Pattern

The Silverlight application that provides the SearchView user interface was developed using the MVVM (Model View ViewModel) pattern, which adds a bit of code but also cleans up the design quite a bit. This is a good pattern to follow if web parts start to become complex, such as in this case. The design is shown in Figure 10.3.

Figure 10.3. MVVM Pattern in the SearchView Web Part

image

The SearchViewSL project has four main .xaml files, each of which defines a View of the application:

• MainPage.xaml provides the primary user interface for SearchView.

• EditPage.xaml provides the editing view.

• PersonDetail.xaml is a child window, which is more or less a dialog box in Silverlight; it shows the social networking user interface when a user clicks on a person in the MainPage view.

• ErrorPage.xaml displays an error message.

Each .xaml file is a view in the MVVM pattern. In classic MVVM style, the views have no code logic, except a couple of lines to create the ViewModel, which might annoy MVVM purists! Each view defines a part of the user interface and uses data binding to connect to application data and behaviors in the ViewModel.

A ViewModel is a class written specifically for a View to bind to. It includes any number of bindable properties and collections that the View can bind to, and it animates the user interface by manipulating those properties and collections. This design has a number of advantages, including providing a better separation of code and design and facilitating automated testing of the ViewModel. Both of these are possible because the ViewModel contains no code that references the View; it can only update the View indirectly through its exposed properties.

The project includes a ViewModel for each of the major views: MainPageVM.cs is for the MainPage.xaml view, EditPageVM.cs is for the EditPage.xaml view, and PersonDetailChildWindow.cs is for the PersonDetailChildWindow.xaml view.

The Model provides the business logic and data and is called by the ViewModel. In some cases, the ViewModel calls web services or client APIs directly, considering them the Model; in others the Model might include complete business and data layers.

Both approaches are used in the sample solution. SharePoint’s web services are complex enough to warrant a SearchService class, which acts as the main page’s Model; however, the user profile and social networking web services are so simple, they’re accessed directly from the PersonDetailVM class. The Edit Page’s ViewModel reads and updates the web part’s settings by accessing a hidden field in the SharePoint Editor Part, so that is the Model when editing the web part.

Each ViewModel implements a number of public properties, which the controls in the corresponding View are bound to. Each ViewModel implements the INotifyPropertyChanged interface, which is used to notify the bound controls that something has changed. The INotifyPropertyChanged interface requires you to implement a single event handler, PropertyChanged, which you need to fire when anything changes that would be visible in the user interface. During data binding, Silverlight notices that INotifyPropertyChanged is available, subscribes to the PropertyChanged event, and refreshes the UI when it fires.

For readability, each ViewModel includes its own implementation of a helper function, SetWithNotify<T>(), which reduces the amount of code in the property declarations. It’s used in the setter of each property, and its job is to fire the PropertyChanged event whenever the property changes. The code is shown in Listing 10.1 along with a sample property; this is at the top of each ViewModel class.

Listing 10.1. Implementing INotifyPropertyChanged


public class MainPageVM : INotifyPropertyChanged
{
    // INotifyPropertyChanged Implementation
    public event PropertyChangedEventHandler PropertyChanged;

    // Helper function to update a property and, if it actually changed,
    // fire the event
    private void SetWithNotify<T>(ref T assignTo, T value,
                                  string propertyName)
    {
        bool valueChanged =
            (assignTo == null || !assignTo.Equals(value));
        assignTo = value;
        if (valueChanged && this.PropertyChanged != null)
        {
            this.PropertyChanged(this,
                new PropertyChangedEventArgs(propertyName));
        }
    }

    // Now a bindable property declaration looks like this:
    private string _userQueryString;
    public string UserQueryString
    {
        get { return _userQueryString; }
        set { SetWithNotify<string>(ref _userQueryString, value,
                                    "UserQueryString"); }
    }
}


The strategy is to store the property values in private fields, such as _userQueryString. When the corresponding property is set, SetWithNotify<T>() first checks to see if the value is actually changing, which is true if the referenced assignTo field was null or unequal to the new value. Then the field is updated with the new value, and if the value had changed, the event is fired.

There are quite a number of properties set up in this way, generally one or more for binding to each control that needs to show or capture information on the corresponding view.

The Search button is only shown when the application is set up to allow users to enter a free-text query, as shown in Figure 10.4. The button is bound to a command with the XAML as shown in this excerpt from MainPage.xaml:

<Button Content="Search"
 Command="{Binding Path=RunSearchQueryCommand}" />

Figure 10.4. SearchView Web Part with all content search and user-entered query

image

RunSearchQueryCommand is a class that implements the ICommand interface at the bottom of the MainPageVM.cs file. When the button is pressed, RunSearchQueryCommand calls a method in the ViewModel to run the search.

Another technique you see is use of the Expression Blend Behavior called CallMethodAction; this is a declarative way to “bind” an arbitrary event to a method in the ViewModel. If Blend is installed, you probably already have the files, which are in C:Program Files (x86)Microsoft SDKsExpressionBlendSilverlightv4.0Libraries; if not, download the SDK at http://bit.ly/SPSL_BlendSDK. If you have Blend 4, reference Microsoft.Expression.Interactions.dll; for Blend 3, reference Expression. samples.interactivity.dll. You need to reference System.Windows.Interactivity and add a couple XML namespaces to your .xaml file:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"

When this is done, it’s easy to “bind” an event in the view to a method in the ViewModel. Listing 10.2 shows an example from MainPage.xaml, which calls the ShowPersonDetail() method in response to a click on the person results ListBox.

Listing 10.2. Using Behaviors to “Bind” a View Event to a ViewModel Method


<ListBox ItemsSource="{Binding Path=SearchResults}"
         Visibility="{Binding Path=PeopleSearchVisible}"
         SelectedItem="{Binding Path=SelectedResult, Mode=TwoWay}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonUp">
            <ei:CallMethodAction MethodName="ShowPersonDetail"
                                 TargetObject="{Binding}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <ListBox.ItemTemplate>
        <DataTemplate>
         ...


Armed with what you already know about data binding and by using Blend behaviors, you can begin to construct cleanly designed MVVM solutions.

In-Place Web Part Editing Experience

SearchView has a lot of configuration information—it allows three kinds of queries (fixed, user-specified, and alphabetic tabs), two kinds of searches (people and all content), and other choices such as the number of results, sort order, and so on. To accommodate this, the web part is edited “in place”—that is, the web part itself is configured right along with all the other settings in the SharePoint Editing panel. Figure 10.5 shows the editing experience. The Editor Part, where settings would normally be edited, appears as just a little text at the top of the editing panel, and the web part itself becomes the main editing surface.

Figure 10.5. Editing the SearchView Web Part

image

All the configuration information is conveniently encapsulated in a class called QueryInfo, which has a base class, SerializableObject, that allows it to be easily transformed in and out of JSON notation. This is used to store the query info as a simple string in SharePoint’s web part store. Chapter 7, “Accessing SharePoint Using the HTML Bridge,” goes into detail on this JSON serialization technique, where it was used to pass data to the PictureView Web Part; in this case it’s only used by the SearchViewSL Silverlight application to store the user’s preferences. Suffice it to say that it’s easy to turn the whole QueryInfo into an HTML-safe string and back again. Listing 10.3 shows the QueryInfo class so you can see what properties can be configured on the web part.

Listing 10.3. The QueryInfo Class holds Web Part Configuration Settings


[DataContract]
public class QueryInfo : SerializableObject
{
    // The constructor fills each property with default values
    public QueryInfo()
    {
        SearchFor = SearchOptions.People;
        FixedQuery = "";
        SelectBy = SelectByOptions.Fixed;
        SortBy = SortByOptions.Alphabetic;
        ResultCount = 10;
    }

    // Search options - what kind of search should we do?
    public enum SearchOptions { People, AllItems };
    [DataMember]
    public SearchOptions SearchFor { get; set; }

    // Fixed query - what fixed query terms should be included
    // in the search?
    [DataMember]
    public string FixedQuery { get; set; }

    // Select by options - should we just use the fixed query terms,
    // or should we include terms from alphabetic tabs or a user-entered
    // query?
    public enum SelectByOptions { Fixed, AlphaTabs, UserQuery }
    [DataMember]
    public SelectByOptions SelectBy { get; set; }

    // Sort by options - how should we sort the results?
    public enum SortByOptions { Alphabetic, Relevance, DateDesc,
                                DateAsc }
    [DataMember]
    public SortByOptions SortBy { get; set; }

    // Result count - how many results should we display?
    [DataMember]
    public int ResultCount { get; set; }
}


The SearchView Web Part (on the server) provides a single string property to store all this information when the web part is edited. The property includes the attributes needed to tell SharePoint to save it along with other web part settings. A second public property, the EditorPartControlID, allows the EditorPart to pass the client-side control ID of a second hidden field over to the web part (more on this in a moment). Listing 10.4 shows both properties. You can view the code in the download in the Chapter10a.sln solution under SearchView in the SearchView.cs file.

Listing 10.4. EditorPartControlID Property


// Web Part Properties
[WebBrowsable(false)]
[Personalizable(PersonalizationScope.Shared)]
public string Query { get; set; } // Serialized QueryInfo

public string EditorPartControlId { get; set; }


Notice that the Query property is marked with the attribute [WebBrowsable(false)], so SharePoint won’t render a text box for the property when the web part is edited. Instead, the SearchView Web Part overrides the CreateEditorPart() method and creates its own Editor Part to handle the editing. This is shown in Listing 10.5.

Listing 10.5. Overriding the CreateEditorParts() Method


public override EditorPartCollection CreateEditorParts()
{
    ArrayList editorPartArray = new ArrayList();

    SearchViewEP editorPart = new SearchViewEP();
    editorPart.ID = this.ID + "_editorPart";
    editorPartArray.Add(editorPart);

    return new EditorPartCollection(editorPartArray);
}


The Editor Part is about as simple as they come, nearly as simple as if it had a single text box for editing the web part property. However, it uses a hidden field and pushes the hidden field’s client ID back into the web part so it can be handed to Silverlight, which actually does the editing. The EditorPart class, found in SearchViewEP.cs in the code download, is shown in Listing 10.6.

Listing 10.6. EditorPart for SearchView


class SearchViewEP : EditorPart
{
    private HiddenField queryInfoFormField = new HiddenField();

    protected override void CreateChildControls()
    {
        base.CreateChildControls();
        // Add the hidden field, and instructions to edit the web
        // part on its surface
        this.Controls.Add(queryInfoFormField);
        this.Controls.Add(new LiteralControl(
            "Please edit SearchView settings in the web part " +
            "directly, then click "OK" or "Apply" below to " +
            "save your changes.<br /><br />"));

        // Pass the hidden field's client ID back to the web part, so
        // it can pass it to Silverlight
        if (this.WebPartToEdit is SearchView)
        {
            SearchView webPart = this.WebPartToEdit as SearchView;
            webPart.EditorPartControlId = queryInfoFormField.ClientID;
        }
    }

    // ApplyChanges - Override to update the web part with the
    // latest EditorPart control values
    public override bool ApplyChanges()
    {
        this.EnsureChildControls();

        if (this.WebPartToEdit is SearchView)
        {
            SearchView webPart = this.WebPartToEdit as SearchView;
            webPart.Query = queryInfoFormField.Value;
        }
        return true;
    }

    // SyncChanges - Override to update the EditorPart controls
    // with the latest web part properties
    public override void SyncChanges()
    {
        this.EnsureChildControls();

        if (this.WebPartToEdit is SearchView)
        {
            SearchView webPart = this.WebPartToEdit as SearchView;
            queryInfoFormField.Value = webPart.Query;
        }
    }
}


The SearchView Web Part passes its QueryInfo property value to Silverlight in a hidden field, and Silverlight configures the web part accordingly. If the web part is in Edit mode, the web part also passes the Editor Part’s hidden field ID so Silverlight can save changes back to SharePoint when the Editor Part is posted back. This is the same as the Editor Part example in Chapter 7 except that a single Silverlight application is handling both the view and edit functions.

Listing 10.7 shows the web part code to pass the QueryInfo via a hidden field and optionally pass the Editor Part’s hidden field as well. Notice that this all needs to be done late in the page rendering cycle, during OnPreRender, to ensure the property is set and the Editor Part has had a chance to create its hidden field and pass its ID in to the web part.

Listing 10.7. Web Part Code to Pass QueryInfo in a Hidden Field


// Fields for child controls
private HiddenField hidden =
    new HiddenField(); // Hidden field to pass QueryInfo to Silverlight
private SilverlightPlugin sl =
    new SilverlightPlugin();

// Create Child Controls
protected override void CreateChildControls()
{
    // Add the hidden field to pass QueryInfo to Silverlight
    this.Controls.Add(hidden);

    // Set up Silverlight plug-in
    SPSite site = SPContext.Current.Site;
    sl.Source = ((site.ServerRelativeUrl == "/") ?
                  "/" : site.ServerRelativeUrl + "/") +
                  "ClientBin/SearchViewSL.xap";
    this.Controls.Add(sl);
}

// OnPreRender event handler - By this time in the page rendering
// process, the EditorPart, if present, will have begun rendering and
// will have assigned its control ID. With that and other information,
// we can pass initial parameters to Silverlight
protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    string initParams = "";

    // Pass the query information to the Silverlight application in a
    // hidden field
    hidden.Value = (this.Query == null) ? "" : this.Query;
    initParams += ",queryFieldId=" + hidden.ClientID;
    // If the editor part is open, pass its control Id
    if (this.EditorPartControlId != null &&
        this.EditorPartControlId != "")
    {
        initParams += ",editorPartFieldId=" + this.EditorPartControlId;
    }

    // Finally, pass the web service endpoint addresses
    SPWeb web = SPContext.Current.Web;
    initParams += ",searchEndpointAddress=" + web.Url +
                                          "/_vti_bin/search.asmx";
    initParams += ",profileEndpointAddress=" + web.Url +
                               "/_vti_bin/userprofileservice.asmx";
    initParams += ",socialEndpointAddress=" + web.Url +
                                "/_vti_bin/socialdataservice.asmx";
    sl.InitParameters = initParams;
}


As you can see, the web part passes in three endpoint addresses, one for each of the three web services to be used. SharePoint web services are in the /_vti_bin/ folder of each and every SharePoint site; SharePoint makes it appear there as a copy of the service in every site even though in reality, a single copy is installed. It’s always best to access web services from within the current site to ensure the client has permission to access the web service (based on their having at least minimal access to the site), as well as to provide the correct context to the web service. That’s why the current site URL is used as a base for each of the endpoint addresses.


Tip

Many people wonder about the choice of folder name, _vti_bin, in SharePoint URLs. The curious acronym “vti” stands for Vermeer Technologies, Inc., the inventors of FrontPage web site editing technology, based in Cambridge, Massachusetts. Microsoft acquired Vermeer in 1996, and portions of the “FrontPage Server Extensions” were used in early versions of SharePoint. The URL remains to this day.

Files that appear in the _vti_bin directory in every SharePoint site are actually installed in the ISAPI folder under the SharePoint root, which by default is C:Program FilesCommon FilesMicrosoft SharedWeb Server Extensions14 for SharePoint 2010.

The tip is: Don’t delete or mess with those _vti folders, or things will break!


When the Silverlight application starts up, it looks to see if it’s in Edit mode or view mode and displays the EditPage or MainPage view accordingly. Listing 10.8 shows the application startup code from App.xaml.cs in the SearchViewSL project. It captures the QueryInfo object and endpoint addresses from InitParams.

Listing 10.8. InitParams Processing in the Application Startup Event


// Global properties
public QueryInfo CurrentQueryInfo { get; set; }
public string SearchEndpointAddress { get; set; }
public string ProfileEndpointAddress { get; set; }
public string SocialEndpointAddress { get; set; }

private void Application_Startup(object sender, StartupEventArgs e)
{
    try
    {
        // Determine edit mode from client state
        bool editMode = false;
        HtmlDocument doc = HtmlPage.Document;
        HtmlElement element =
            doc.GetElementById("MSOSPWebPartManager_DisplayModeName");

        if (element != null && element.GetAttribute("value") != null)
        {
            string s = element.GetAttribute("value").ToString();
            if (s.ToLower() == "edit") editMode = true;
        }

        // Get the query settings from the hidden field referenced in
        // InitParams
        string serializedQuery =
            GetInitParamValue(e, "queryFieldId", true);
        this.CurrentQueryInfo = GetQuery(serializedQuery);

        // Get editor part field id, if present
        string editorPartFieldId =
            GetInitParamValue(e, "editorPartFieldId", false);

        // Get endpoint addresses for web service calls
        this.SearchEndpointAddress =
            GetInitParamValue(e, "searchEndpointAddress", true);
        this.ProfileEndpointAddress =
            GetInitParamValue(e, "profileEndpointAddress", true);
        this.SocialEndpointAddress =
            GetInitParamValue(e, "socialEndpointAddress", true);
        // Now that we have all the necessary information, bring up
        // the UI
        if (editMode)
        {
            // We are editing; ensure we have a queryInfo and run
            // the Edit page
            if (this.CurrentQueryInfo == null)
            {
                this.CurrentQueryInfo = new QueryInfo();
            }
            this.RootVisual =
                new EditPage(this.CurrentQueryInfo, editorPartFieldId);
        }
        else
        {
            // We are viewing; if we don't have a QueryInfo, throw an
            // exception. If we do, then run the Main page.
            if (this.CurrentQueryInfo == null)
            {
                throw new Exception("Please edit the web part to set " +
                                    "up a Search View");
            }
            this.RootVisual = new MainPage();
        }
    }
    catch (Exception ex)
    {
        this.RootVisual = new ErrorPage(ex.Message);
    }
}


You might notice there is a trick for determining if the page is being edited. SharePoint maintains an HTML element called MSOSPWebPartManager_DisplayModeName that contains the edit state and can be easily read by Silverlight using the HTML Bridge. Most of the rest of the code is involved with retrieving data out of InitParams; in the end the application decides if it should show the main, edit, or error page depending on what it finds there.

Listing 10.9 shows the helper functions used the in the previous listing. The first extracts values from InitParams, and the second deserializes the QueryInfo structure for passing to the ViewModel.

Listing 10.9. Helper Functions for InitParams and QueryInfo Processing


// GetInitParamValue - extracts a value from InitParms, optionally
// throwing an exception if it is missing
private string GetInitParamValue(StartupEventArgs e, string key,
                                 bool throwExceptionIfMissing)
{
    string value = null;
    if (e.InitParams.ContainsKey(key))
    {
        value = e.InitParams[key];
    }
    else if (throwExceptionIfMissing)
    {
        throw new Exception("The web part did not pass the correct " +
            "initial parameters to the Silverlight application");
    }
    return value;
}

// GetQuery - De-serializes the QueryInfo class passed in from the host
private QueryInfo GetQuery(string queryFieldId)
{
    QueryInfo result = null;

    if (queryFieldId != "")
    {
        // Use the browser DOM to find the element, then parse it
        HtmlDocument doc = HtmlPage.Document;
        HtmlElement element = doc.GetElementById(queryFieldId);
        if (element != null && element.GetAttribute("value") != null)
        {
            string serializedQueryInfo =
                element.GetAttribute("value").ToString();
            if (serializedQueryInfo != "")
            {
                result = QueryInfo.Load<QueryInfo>(serializedQueryInfo);
            }
        }
    }
    return result;
}


In Edit mode, the EditPageVM ViewModel interacts with the hidden field in the server-side editor part. The ViewModel writes the whole serialized QueryInfo back into the editor part every time anything on the form changes. If the user saves the changes, the editor part saves it for future display. Now there are two things to do when a property changes: As before, the PropertyChanged event must be fired, and also the editor part field must be updated.

As an example, Listing 10.10 shows the declaration of the SelectBy property in EditPageVM.cs and the method it calls to update the editor part’s hidden field. When the user changes the Search By radio buttons in the Edit Page view, the two-way binding writes to the property. The property setter immediately calls UpdateEditorPart() to update the hidden field, so if the next user click is the OK button, the change is saved back to SharePoint.

Listing 10.10. Updating the EditorPart Hidden Field in a ViewModel Property Setter


// Example of a bindable property in the EditPage ViewModel
private QueryInfo.SelectByOptions _selectBy;
public QueryInfo.SelectByOptions SelectBy
{
    get { return _selectBy; }
    set
    {
        SetWithNotify<QueryInfo.SelectByOptions>(ref _selectBy, value,
            "SelectBy");
        UpdateEditorPart();
    }
}

// . . .

// Method to update the field in the editor part for saving back to
// the server; this should be called whenever a UI choice is changed
private void UpdateEditorPart()
{
    QueryInfo queryInfo = new QueryInfo();

    queryInfo.SearchFor = this.SearchFor;
    queryInfo.SelectBy = this.SelectBy;
    queryInfo.FixedQuery = this.FixedQuery;
    queryInfo.ResultCount = this.ResultCount;
    queryInfo.SortBy = this.SortBy;

    string jsonQueryInfoString = queryInfo.Serialize();

    // Use the browser DOM to find the element and parse its contents
    HtmlDocument doc = HtmlPage.Document;
    HtmlElement element = doc.GetElementById(this.editorPartFieldId);
    if (element != null)
    {
        element.SetAttribute("value", jsonQueryInfoString);
    }
}


Accessing Enterprise Search

Enterprise Search has been a major part of SharePoint from the very beginning, and over the years it’s become more and more powerful. The SharePoint search engine pre-dates SharePoint itself and was originally released as part of Microsoft Site Server 3.0 back in the late 1990s. It’s improved with each version and is now a scalable search engine capable of finding documents, web pages, and people in a wide range of repositories. In addition to SharePoint itself, the SharePoint search engine can index content in file shares, crawlable web sites, Microsoft Exchange public folders, Lotus Notes databases, and business data in the form of Business Connectivity Services entities.

In January 2008, Microsoft acquired a high-end enterprise search vendor, a Norwegian firm called FAST Search and Transfer. FAST offers greater scalability than the SharePoint search engine and includes a number of high-end features such as entity extraction (extracting metadata from document text), deep refiners (ability to refine the search based on metadata about the whole result set, with hit counts) and more advanced linguistics support. The FAST search engine is available for SharePoint Server in a product called FAST Search Server 2010 for SharePoint, greatly simplifying setup and configuration compared to earlier versions.

The good news is that both search engines have a common API, so the examples in this chapter work with either one.

Please don’t confuse these enterprise search solutions with the free site-by-site search that’s included with SharePoint Foundation! The search code in this chapter requires either the SharePoint or FAST for SharePoint search engines, as found in any of the following products:

Microsoft Search Server 2010 Express—This is a free download and allows a single server to run the standard SharePoint search engine along with SharePoint Foundation 2010.

Microsoft SharePoint Server 2010—This comes in standard and enterprise editions, both of which include the SharePoint search engine and the ability to build multiserver search environments with high availability and scalability.

Microsoft FAST Search Server 2010 for SharePoint—This is an addition to the enterprise edition of Microsoft SharePoint Server 2010.

Keyword Query Language

SharePoint has three query languages: a SQL variant (supported only by the SharePoint standard search engine), Fast Query Language (supported only by FAST) and Keyword Query Language (which works with both). For most applications, Keyword Query Language (KQL) is the syntax of choice, and it’s also easy to enter into a search box.

A KQL query consists of free-text keywords, property-restriction keywords, and operators for Boolean, proximity, wildcards and so on. To search for the word dog, the KQL query is simply “dog”. To search for dog in the Title property, add a property restriction and search for “title:dog”. To search for documents with cat in the body of the document and dog in the Title, search for “cat and title:dog”.

Part of the idea of the SearchView Web Part is that it allows part of the KQL query to be set when the site administrator configures the web part and another part by the end user at runtime. For example, if the web part is configured to allow the user to enter a search query, one or more property-restriction keywords can be added when the web part is edited to make a specialized search. The term “site:url” limits results to documents beneath the URL, “fileextension:ext” limits results to the “ext” file extension, “department:sales” limits a people search to people in the sales department.

When the web part is configured for fixed search only, just the search terms entered during web part editing are used and the user can’t add anything. When it’s configured for user search, the user has an auto-complete box to enter a query, and the fixed terms are added as well. When it’s configured for alphabetic tabs, a wildcard search term is added to the fixed search terms, with the lastname property restriction for people searches. For example, clicking the S tab does a KQL query for “lastname:s*”.

For a full reference on KQL, see the Keyword Query Syntax Reference at http://msdn.microsoft.com/en-us/library/ee558911.aspx.

Accessing the Search Web Service

The first step to accessing the search web service from a Silverlight application is to add a web reference. To do this, right-click the References folder in Visual Studio’s Solution Explorer and select Add Service Reference. Enter the URL for your search service, which should be your debugging site (for now) with the path _vti_bin/search.asmx under the site as shown in Figure 10.6. Call your service SearchQueryService.

Figure 10.6. Adding a reference to the Search.asmx service

image

With the service reference in place, you’re ready to set up and invoke search queries using the service.

Invoking a Search Query

Before invoking the QueryAsync() method on the search web service, there’s some preparation to do. QueryAsync() expects a search query packet, which is an XML structure containing all the details about the search to be performed. The KQL query is just one of many elements in the packet. Listing 10.11 shows an example that does a general search for “dog” and returns the first ten results.

Listing 10.11. Sample Search Query Packet XML


<QueryPacket xmlns="urn:Microsoft.Search.Query">
  <Query>
    <QueryId />
    <SupportedFormats>
      <Format revision='1'>
urn:Microsoft.Search.Response.Document:Document</Format>
    </SupportedFormats>
    <Context>
      <QueryText language='en-US' type='STRING'>dog</QueryText>
    </Context>
    <Range>
      <StartAt>1</StartAt>
      <Count>10</Count>
    </Range>
    <Properties>
      <!-- Mandatory Properties -->
      <Property name="Title" />
      <Property name="Path" />
      <Property name="Description" />
      <Property name="Write" />
      <Property name="Rank" />
      <Property name="Size" />
      <!-- Optional Properties for Documents -->
      <Property name="HitHighlightedSummary" />
    </Properties>
    <SortByProperties>
      <SortByProperty name="rank" direction="Descending" />
    </SortByProperties>
  </Query>
</QueryPacket>


The SearchViewSL Silverlight application contains pre-built query packets to be used as a starting point. They can be found as XML files in the QueryServiceXml folder, and they’re deployed as part of the .xap package for use in the SearchService code. There are separate templates for People vs. All Sites or general content searches, and they differ mainly in the properties they return. Only the properties with elements under the <Properties> tag are returned, and the search fails if the mandatory properties aren’t there at a minimum. Note that the search engine is only capable of returning managed properties, which can be set up in the search service administration pages in SharePoint Centeral Administration.

The SearchService class, in SearchService.cs, implements the search data model. It sets up one of these query packets during initialization and stuffs information from the web part setup into the XML from a QueryInfo object. Loading the query packet template is simple in Silverlight with LINQ to XML, as shown in Listing 10.12.

Listing 10.12. Code to Load a Query Packet XML Template in SearchService.cs


// Given queryInfo from the server, load the QueryPacket template
// from this project
if (queryInfo.SearchFor == QueryInfo.SearchOptions.People)
{
    this.currentQueryPacket =
        XDocument.Load("QueryServiceXml/QueryPacket-People.xml");
}
else
{
    this.currentQueryPacket =
        XDocument.Load("QueryServiceXml/QueryPacket-AllSites.xml");
}


Following this, in a method called InitializeQueryPacket(), the various query settings are added to the generic query XML. First, the code checks for a People search and adds a KQL term, “scope:People” to the query packet. It also sets up the fixed portion of the query; the user portion isn’t known yet because this is still initialization. Listing 10.13 shows InitializeQueryPacket().

Listing 10.13. Initializing the Query Packet XML


private void InitializeQueryPacket
             (QueryInfo queryInfo, XDocument queryPacket)
{

    // Handle SearchFor People vs. All Sites
    if (queryInfo.SearchFor == QueryInfo.SearchOptions.People)
    {
        XElement queryTextElement =
            queryPacket.Descendants(sq + "QueryText").First<XElement>();
        queryTextElement.Value =
            (queryTextElement.Value + " scope:People").Trim();
    }

    // Handle fixed portion of query
    if (queryInfo.FixedQuery != "")
    {
        XElement queryTextElement =
            queryPacket.Descendants(sq + "QueryText").First<XElement>();
        queryTextElement.Value =
           (queryTextElement.Value + " " + queryInfo.FixedQuery).Trim();
    }

    // Handle SortBy options
    XElement sortByElement =
       queryPacket.Descendants(sq + "SortByProperty").First<XElement>();
    switch (queryInfo.SortBy)
    {
        case QueryInfo.SortByOptions.Alphabetic:
            {
                sortByElement.Attribute("name").Value = "LastName";
                sortByElement.Attribute("direction").Value =
                    "Ascending";
                break;
            }
        case QueryInfo.SortByOptions.DateAsc:
            {
                sortByElement.Attribute("name").Value = "write";
                sortByElement.Attribute("direction").Value =
                    "Ascending";
                break;
            }
        case QueryInfo.SortByOptions.DateDesc:
            {
                sortByElement.Attribute("name").Value = "write";
                sortByElement.Attribute("direction").Value =
                    "Descending";
                break;
            }
        case QueryInfo.SortByOptions.Relevance:
            {
                sortByElement.Attribute("name").Value = "rank";
                sortByElement.Attribute("direction").Value =
                    "Descending";
                break;
            }
    }

    // Handle ResultCount
    int resultCount = queryInfo.ResultCount;
    XElement resultCountElement = queryPacket.Descendants(sq +
        "Count").First<XElement>();
    resultCountElement.Value = resultCount.ToString();
}


At some point the user will click a tab or the Search button, or the Main Page’s ViewModel will decide to automatically issue a “fixed-only” query at the end of its initialization. The SearchService class has methods for each of these, all using different signatures of RunQuery(): One inserts the user query string, and another inserts the alphabetic wildcard query into the query packet before making the request. Listing 10.14 shows the RunQuery() method for a user query and the final invocation of the web service.

Listing 10.14. Running a User Query in SearchService.cs


public void RunQuery(string userQueryString)
{
    // Copy the current query packet and inject the user's text
    XDocument q = new XDocument(currentQueryPacket);
    XElement queryTextElement = q.Descendants(sq +
        "QueryText").First<XElement>();
    queryTextElement.Value = queryTextElement.Value + " " +
        userQueryString;

    RunQuery(q);
}

private void RunQuery(XDocument queryPacket)
{
    BasicHttpBinding httpBinding = new BasicHttpBinding();
    httpBinding.MaxReceivedMessageSize = 500000;
    httpBinding.MaxBufferSize = 500000;
    EndpointAddress endpoint =
        new EndpointAddress(this.searchEndpointAddress);

    QueryServiceSoapClient queryClient =
        new QueryServiceSoapClient(httpBinding, endpoint);
    queryClient.QueryCompleted +=
        new EventHandler<QueryCompletedEventArgs>
        (queryClient_QueryCompleted);

    queryClient.QueryAsync(queryPacket.ToString());
}


Notice that the local endpoint address (URL of the .asmx file) is passed to the QueryServiceSoapClient and replaces the one set in Visual Studio. Also notice that somewhat large results are expected, with the maximum received message and buffer sizes set to 500K bytes.

As with all Silverlight networking, the request is asynchronous. Therefore, an event handler is set up to receive the completion at queryClient_QueryCompleted. The next section explains what happens when the query completes and the event is fired.

Handling Query Completion

Search.asmx returns its results in an XML structure; a subset set is shown in Listing 10.15.

Listing 10.15. Sample Search Results XML


<ResponsePacket xmlns="urn:Microsoft.Search.Response">
  <Response>
    <Range>
      <StartAt>1</StartAt>
      <Count>4</Count>
      <TotalAvailable>4</TotalAvailable>
      <Results>
        <Document xmlns="urn:Microsoft.Search.Response.Document">
          <Action>
            <LinkUrl
fileExt="aspx">http://oblio/my/Person.aspx?accountname=virtualrian</LinkUrl>
          </Action>
          <Properties xmlns="urn:Microsoft.Search.Response.Document.Document">
            <Property>
              <Name>Title</Name>
              <Type>String</Type>
              <Value>Brian Burke</Value>
            </Property>
            <Property>
              <Name>Path</Name>
              <Type>String</Type>
<Value>http://oblio/my/Person.aspx?accountname=virtualrian</Value>
            </Property>
            <Property>
              <Name>Write</Name>
              <Type>DateTime</Type>
              <Value>2010-06-29T20:42:20</Value>
            </Property>
            <Property>
              <Name>Rank</Name>
              <Type>Int64</Type>
              <Value>62174552</Value>
            </Property>
            <Property>
              <Name>Size</Name>
              <Type>Int64</Type>
              <Value>0</Value>
            </Property>
          </Properties>
        </Document>
        <!-- More documents were removed for brevity -->
      </Results>
    </Range>
    <Status>SUCCESS</Status>
  </Response>
</ResponsePacket>


As you can see, it’s a somewhat verbose format with property names and values in subelements. Although this contains the data you need, the format isn’t suitable for binding to a Silverlight control. Instead, the Main Page ViewModel expects to bind its search results to a class called ResultsItem, defined in ResultsItem.cs and shown in Listing 10.16.

Listing 10.16. ResultsItem Class for View Binding


// Results Item used to bind search results to the View
public class ResultsItem
{
    public string Title { get; set; }
    public string UserName { get; set; }
    public string Url { get; set; }
    public string Summary { get; set; }
    public string Date { get; set; }
    public string Size { get; set; }
    public string JobTitle { get; set; }
    public string Department { get; set; }
    public string WorkPhone { get; set; }
    public string Email { get; set; }
    public string EmailUrl { get; set; }
    public string PictureUrl { get; set; }
}


When the search query completes, the XML results need to be converted into a collection of ResultsItem for binding, and then SearchService fires its SearchComplete event to tell the ViewModel that the results are ready. Listing 10.17 shows the queryClient_QueryCompleted event handler, which does this transformation.

Listing 10.17. Query Completed Event Handler


// This event fires when the web service request is completed
void queryClient_QueryCompleted(object sender,
                                QueryCompletedEventArgs e)
{
    // Parse the results and check for errors
    XDocument resultsXml = XDocument.Parse(e.Result.ToString());

    string status = resultsXml.Descendants(sr +
        "Status").First<XElement>().Value;
    var x = resultsXml.Descendants(sr+ "DebugErrorMessage");
    if (x.Count<XElement>() > 0 && x.First<XElement>().Value != "")
    {
        status += " " + x.First<XElement>().Value;
    }

    // If the results are successful, display them
    if (status.ToLower() == "success")
    {
        // Project the results into a collection of result items
        IEnumerable<ResultsItem> resultItems =
                from r in resultsXml.Descendants(srd + "Document")
                select new ResultsItem
        {
             Title = getPropertyValue(r, "Title"),
             UserName = getPropertyValue(r, "AccountName"),
             Url = getPropertyValue(r, "Path"),
             Summary = getSummary(r),
             Date = convertDate(getPropertyValue(r, "Write")),
             Size = convertSize(getPropertyValue(r, "Size")),
             JobTitle = getPropertyValue(r, "JobTitle"),
             Department = getPropertyValue(r, "Department"),
             WorkPhone = getPropertyValue(r,"WorkPhone"),
             Email = getPropertyValue(r,"WorkEmail"),
             EmailUrl = "Mailto:" + getPropertyValue(r, "WorkEmail"),
             PictureUrl = convertPictureUrl(
                                      getPropertyValue(r, "PictureURL"))
        };
        // Fire the event that the search is complete
        this.SearchComplete(this,
                new SearchCompleteEventArgs (true, resultItems, ""));
    }
    else
    {
        // If here, we had an error. If it's just results not found,
        // report success with no results. If something else
        // happened, report an error status.
        if (status == "ERROR_NO_RESULTS_FOUND")
        {
            this.SearchComplete (this,
                new SearchCompleteEventArgs(true, null, ""));
        }
        else
        {
             this.SearchComplete (this,
                 new SearchCompleteEventArgs (false, null, status));
        }
    }
}


You might have noticed in Listing 10.15 that there’s a Status XML element at the end of the search results XML that indicates if the request worked or not. A status of “SUCCESS” means that results were found; a status of “EROR_NO_RESULTS_FOUND” is the normal case of an empty result set, and other values indicate various error conditions. Part of parsing the results XML is to determine and handle the request status.

A helper function, getPropertyValue(), is used to extract property values from the search results XML. It uses an XPath query to locate a requested property and return its value as a string and is shown in Listing 10.18.

Listing 10.18. Helper Function to Extract Property Values from Search Results XML


// Namespaces for the search results
private XNamespace sr = "urn:Microsoft.Search.Response";
private XNamespace srd = "urn:Microsoft.Search.Response.Document";
private XNamespace srdd =
                      "urn:Microsoft.Search.Response.Document.Document";

// Utility function to extract a property value from the results set.
// The results are provided in this format:
//
// <Property>
//     <Name>Write</Name>
//     <Type>DateTime</Type>
//     <Value>2010-06-29T20:42:20</Value>
// </Property>
//
private string getPropertyValue(XElement element, string propertyName)
{
    // Set up namespace manager for all the various namespaces used in
    // the results
    XmlNamespaceManager namespaceManager =
        new XmlNamespaceManager(new NameTable());
    namespaceManager.AddNamespace("sr", sr.NamespaceName);
    namespaceManager.AddNamespace("srd", srd.NamespaceName);
    namespaceManager.AddNamespace("srdd", srdd.NamespaceName);

    // Copy the element to work around issue where (XPath)
    // extensions don't iterate correctly when called from the LINQ
    // query
    XElement x = new XElement(element);

    // Use XPath to find the <Property> element that contains a <Name>
    // matching our property name
    string xpath = "//srdd:Property[srdd:Name = "" + propertyName +
                   ""]";
    XElement matchingElement = x.XPathSelectElement(xpath,
                               namespaceManager);

    // If such an element was found, return its value.
    string result = "";
    if (matchingElement != null)
    {
        result = matchingElement.Element(srdd + "Value").Value;
    }

    return result;
}


If the query was successful or just returned a “no results” error, the queryClient_QueryCompleted event handler fires its SearchComplete event handler to tell the caller (the ViewModel) the request is completed. Because the SearchService class has already provided the results as a collection of ResultsItems, and the view is bound to such a collection, the ViewModel only needs to copy its collection to its SearchResults property to display the results. Listing 10.19 shows the SearchComplete event handler in the MainPageVM.cs.

Listing 10.19. Search Complete Event Handler in MainPage ViewModel


void searchService_SearchComplete(object sender,
                                  SearchCompleteEventArgs e)
{
    if (e.Succeeded)
    {
        this.SearchResults = e.SearchResults;
    }
    else
    {
        MessageBox.Show(e.ErrorMessage);
    }
}


Search Suggestions

SharePoint 2010 includes a cool new feature called Search Suggestions, which provides suggested search queries as the user types. It’s possible to utilize this feature via the same Search.asmx web service. If you modify the web part to allow selecting by User Query, a text box is shown for the user query, but it’s not really a text box. It’s actually a user control, SearchSuggestionsBox.xaml. This small component does not follow the MVVM pattern but simply calls the Search web service from code-behind.

SearchSuggestionsBox’s user interface is a single control, an AutoCompleteBox, which thankfully already implements the UI logic to offer suggestions when a user pauses in typing and allows him to select a suggestion. This is the same AutoCompleteBox used in Chapter 9, “Accessing SharePoint Data with WCF Data Services,” from the Silverlight Toolkit on Codeplex.

<my:AutoCompleteBox x:Name="UserQueryAutoCompleteBox"
             Text="{Binding Path=UserQueryString, Mode=TwoWay}"
             MinimumPopulateDelay="250" MinimumPrefixLength="1"
             Populating="UserQueryAutoCompleteBox_Populating" />

When the user is typing and enters at least MinimumPrefixLength characters (1 in this case) and delays for the MinumPopulateDelay interval (250 milliseconds in the sample), the Populating event fires and the search suggestions are shown. Figure 10.7 shows the Search Suggestions in action.

Figure 10.7. Search Suggestions in the SearchView Web Part

image

Before this will work on your server, you need to ensure you have some search suggestions in the system. Suggestions are based on user queries in which the user actually clicks one of the results in a SharePoint Search Center site; the idea is if the query is useful, the user clicks at least one of the results. If a particular query’s result is clicked at least six times within a year, it becomes a search suggestion. Be sure to run the Prepare Query Suggestions timer job (or wait for it to run) after all the clicking in order to see your search suggestions.

A quicker approach is to use Windows PowerShell to create search suggestions manually. To do this, open the SharePoint 2010 Management Shell on the Start menu under All Programs/Microsoft SharePoint 2010 Products and enter the following commands:

$ssa=Get-SPEnterpriseSearchServiceApplication
New-SPEnterpriseSearchLanguageResourcePhrase -SearchApplication $ssa
    -Language en-US -Type QuerySuggestionAlwaysSuggest -Name "SharePoint"
$timer=Get-SPTimerJob|? {$_.Name -eq "Prepare Query Suggestions"}
$timer.RunNow()

To list the currently available search suggestions, use these commands. (You only need to run the first one once, so if you just added a search suggestion you can skip it.)

$ssa=Get-SPEnterpriseSearchServiceApplication
Get-SPEnterpriseSearchQuerySuggestionCandidates -SearchApp $ssa

When the user pauses typing in the search box, the AutoCompleteBox fires its Populating event, and the code-behind in SearchSuggestionBox.xaml.cs calls the Search web service for suggestions. The web service call is very similar to the general Search query: An XML request packet is created, and the GetQuerySuggestionsAsync() method is called. See the sample code for the details.

When the response comes back, it is bound to the AutoCompleteBox to show the suggestions as shown in Listing 10.20. Thankfully, the results are simply a collection of strings that don’t require any elaborate transformation in order to bind them. If there are no suggestions, an empty collection is returned, and nothing is shown. Notice that after binding, the event handler must call the PopulateComplete() method of the AutoCompleteBox to notify it that new suggestions have been bound to its ItemsSource.

Listing 10.20. Handling the Search Suggestions Results in SearchSuggestionsBox.xaml.cs


// Event handler called when search suggestions have been obtained
// from the server
private void queryClient_GetQuerySuggestionsCompleted
                (object sender, GetQuerySuggestionsCompletedEventArgs e)
{
    Dispatcher.BeginInvoke(() =>
    {
        if (e.Error == null)
        {
            // We have an array of strings, easily bound to the text box
            UserQueryAutoCompleteBox.ItemsSource = e.Result;
            UserQueryAutoCompleteBox.PopulateComplete();
        }
    });
}


Accessing Social Data

A major area of advancement in SharePoint 2010 is in its social networking capabilities. The SearchView Web Part can display some of this information when a person is clicked in the People search results ListBox, as shown in Figure 10.2. All this work is done in PersonDetailChildWindowVM.cs, which is the ViewModel for the Person Detail child window.

This section is a great example of why asynchronous networking is a good thing, even if the need to put completion logic in event handlers is annoying at times. The Person Detail ViewModel issues two requests asynchronously: one for the user profile (displayed on the left) and one for the user’s activity feed (displayed on the right). Because they are asynchronous, there is no extra work to run them in parallel, and the user interface is updated dynamically with the information as it is returned.

Accessing the User Profile Service

All the user data in the left portion of the Person Detail child window comes from the User Profile Service. The User Profile Service provides access to SharePoint’s data about end-users, which is normally imported from Active Directory (or some other directory source) and augmented with information entered by users in their Profile page. To see your profile, click your name in the upper-right corner of most any SharePoint screen and select My Profile. To see just the profile properties, click Edit My Profile under your picture.

Begin by adding a service reference to the user profile service in your development site under /_vti_bin/userprofileservice.asmx. Call the service reference UserProfileService.

In an ideal world this would be enough, but in Silverlight 4 the world was not yet ideal, so there’s a problem you might need to work around. It turns out that .asmx web services have a different notion of certain data types (namely guid and char) than Silverlight 4 does, and thus Silverlight chokes on any .asmx web service that uses them. Note that this problem is fixed in Silverlight 5, even when Silverlight 5 is running a Silverlight 4 application. Therefore if you’re running Silverlight 5 but developing for Silverlight 4, be sure to include the work-around. The work-around is benign and has no known problems with Silverlight 5.

The UserProfileService.asmx has this issue. The solution is to add a behavior to the service reference manually, as described in the Silverlight Team Blog on May 26, 2010, in a posting called, “Workaround for accessing some ASMX services from Silverlight 4.” The code download has this solution already in place in the form of the AsmxBehavior.cs and AsmxMessageInspector.cs files. The behavior is added to the web service request in a single line of code, which is highlighted later on in this section. The blog posting can be found at http://bit.ly/SPSL_AsmxWorkaround.

At the end of its initialization, the Person Detail ViewModel (PersonDetailChildWindowVM.cs) calls RequestProfileInfo(userName) to issue a request for the user’s profile. This is shown in Listing 10.21.

Listing 10.21. Calling the User Profile Web Service from Silverlight 4


// RequestProfileInfo - Requests a user's profile information
public void RequestProfileInfo(string userName)
{
    // Initiate populating the Profile Information
    BasicHttpBinding httpBinding = new BasicHttpBinding();
    EndpointAddress endpoint =
        new EndpointAddress(this.profileEndpointAddress);

    UserProfileServiceSoapClient profileClient =
        new UserProfileServiceSoapClient(httpBinding, endpoint);
    profileClient.GetUserProfileByNameCompleted +=
        new EventHandler<GetUserProfileByNameCompletedEventArgs>
            (GetProfilebyName_Completed);
    profileClient.Endpoint.Behaviors.Add(new AsmxBehavior());
    profileClient.GetUserProfileByNameAsync(userName);
}


Notice the second to last line adds the AsmxBehavior to the request endpoint; this institutes the work-around mentioned earlier. Otherwise the request is similar to the Search example and much simpler without the need to set up a complex XML request packet!

The request returns an ObservableCollection<PropertyData>, which is basically a collection of properties. The GetProfilebyName_Completed event handler extracts the user’s name and the URL of his picture and stuffs them into ViewModel properties, which are bound to the user interface. Other properties are displayed in a DataGrid, which is bound to a collection of property name-value pairs in the View.

A simple Dictionary is used to call out the interesting properties and give them user-friendly names; this is used in a LINQ query to transform the results into the property name-value pairs needed to bind to the DataGrid. The Dictionary is shown in Listing 10.22 and is defined in PersonDetailChildWindow.xaml.cs.

Listing 10.22. PropertyMap Dictionary Manages Properties to Be Displayed


// This dictionary will be used to map internal property names to
// display names. Properties not found here will not be rendered.
private Dictionary<string, string> PropertyMap =
                              new Dictionary<string, string>
{
    {"WorkPhone", "Work Phone"},
    {"Department", "Department"},
    {"Title", "Title"},
    {"WorkEmail", "Email"},
    {"SPS-Location", "Location"},
    {"SPS-StatusNotes", "Message"}
};


Listing 10.23 shows the event handler that is called with the user profile results. Basically all it does is copy user profile properties from the result into the ViewModel’s Properties collection, which is bound to the View. Because the Properties collection is bound to the View in PersonDetailChildWindow.xaml, the results are displayed without any further action.

Listing 10.23. Copying User Profile Properties to the ViewModel’s Properties Collection


public void GetProfilebyName_Completed(Object sender,
                              GetUserProfileByNameCompletedEventArgs e)
{
    // Get the properties
    ObservableCollection<PropertyData> result = e.Result;

    // Extract the Preferred Name
    IEnumerable<PropertyData> pdata = from property in result
                                  where property.Name == "PreferredName"
                                  select property;
     this.PreferredName =
         pdata.First<PropertyData>().Values[0].Value.ToString();
    this.WindowTitle = "Details for " + this.PreferredName;
    // Extract the Picture URL (or substitute dummy if it's not
    // provided)
     pdata = from property in result
             where property.Name == "PictureURL"
             select property;
     if (pdata.First<PropertyData>().Values.Count > 0)
    {
        this.PictureUrl =
           pdata.First<PropertyData>().Values[0].Value.ToString();
    }
    else
    {
        this.PictureUrl =
            "/_layouts/images/O14_person_placeHolder_192.png";
    }

    // Extract other properties into the ViewModel's Properties
    // collection, based on the PropertyMap dictionary
    this.Properties = from property in result
                       where (property.Values.Count > 0)
                          && PropertyMap.ContainsKey(property.Name)
                       orderby PropertyMap[property.Name]
                       select new PersonProperty
                       {
                           DisplayName = PropertyMap[property.Name],
                           Value = property.Values[0].Value.ToString()
                       };

    // Let the rest of the world know that profile data has been
    // received!
    this.ProfileInfoReceived = true;
}


Accessing the Activity Feed

SharePoint 2010 includes an Activity Feed feature that lets users know what other users have been doing. Rating a file, commenting on another user’s note board, posting to a blog, or adding a colleague all generate activities in the user’s Activity Feed. You can see the Activity Feed in the right of Figure 10.2.

SharePoint displays the activity feed on each user’s My Site. To see the activities, find a user in SearchView and click his or her My Site link. The activities are shown under the Recent Activites heading. The activities won’t show up until the Activity Feed timer job runs; to run this, issue the following power shell commands (with the first two lines all on one line as you type):

$timer=Get-SPTimerJob|?
        {$_.Name -eq "User Profile Service Application_ActivityFeedJob "}
$timer.RunNow

You can also run the Activity Feed timer job in SharePoint Central Administration. Under Central Administration click Monitoring; then under the Timer Jobs heading, click Review Job Definitions. Find the job called User Profile Service Application—Activity Feed Job; click it and then at the bottom of the job details page, click the Run Now button.

The activity feed is available as a simple ATOM feed at the My Site root, with the username included. For example, http://intranet.contoso.com/my/_layouts/activityfeed.aspx?consolidated=false&publisher=contoso%5 Crong is the activity feed for user contoso ong.

At the end of initialization, the Person Detail child window calls RequestActivityFeed(userName) to request the data to display the user’s activity feed. As you can see in Listing 10.24, only a few lines of code are needed to make a web request for the feed. The URL to request is based on the person’s My Site URL and the user name, both of which were passed in from the search results.

Listing 10.24. Requesting the User’s Activity Feed


// RequestActivityFeed - Kick off the request for an activity (ATOM)
// feed
private void RequestActivityFeed(string userName)
{
    string feedUrl = this.userUrl.Substring(0,
            this.userUrl.IndexOf("Person.aspx")) +
            "_layouts/activityfeed.aspx?consolidated=false&publisher=" +
            System.Windows.Browser.HttpUtility.UrlEncode(userName);

    WebClient wc = new WebClient();
    wc.OpenReadCompleted +=
        new OpenReadCompletedEventHandler(RequestActivityFeedCompleted);
    wc.OpenReadAsync(new Uri(feedUrl));
}


When the response comes back, the OpenReadCompleted event handler is called. Silverlight provides a SyndicationFeed object to make parsing a breeze. From there, the items can be transformed into ActivityItems, which are bound to a ListBox in the view. When the Activities property in the ViewModel is set, it fires the PropertyChanged event to tell the view to display the data, as shown in Listing 10.25.

Listing 10.25. Handling the Activity Feed Completion


// Handle the activity feed when it comes back
void RequestActivityFeedCompleted(object sender,
                                        OpenReadCompletedEventArgs e)
{
    // Transform the result stream into a SyndicationFeed for easy
    // ATOM parsing
    XmlReader feedReader = XmlReader.Create(e.Result);
    SyndicationFeed feed = SyndicationFeed.Load(feedReader);

    // Project a collection of ActivityItems for binding to the View
    this.Activities = from item in feed.Items
                      select new ActivityItem
                      {
                          Title = item.Title.Text,
                          Description = FixHtml (item.Summary.Text),
                          PubDate = item.PublishDate.LocalDateTime
                      };
}


Adding Social Comments

One of SharePoint 2010’s new social networking features is the ability to comment on most anything in SharePoint. It’s easy to comment on documents, wiki pages, and the like. Under the covers, the comments are stored in the User Profile database. Each comment is associated with the user who made it and the URL of the item he is commenting on.

If the URL associated with a comment happens to point to a user profile, this becomes a comment on the user and is displayed on the user’s note board. Through the Person Detail child window, you can enter a short note and click a button to send it to the user you’re viewing; you can see this in the lower right corner of Figure 10.2.

Social comments are managed via the Social Data web service. As before, add a service reference from your Silverlight project, this time for _vti_bin/SocialDataService.asmx. The same .asmx Behavior work-around for using Silverlight 4 with the User Profile service is required for the Social Data service as well.

When the button is pressed, a Command object fires and ultimately calls the PostNote() method in PersonDetailChildWindow.xaml.cs. As you can see in Listing 10.26, the code creates a simple anonymous event handler to clear the note text when the service returns. Otherwise it’s about as simple as calling the AddCommentsAsync() method when the binding and endpoint are set up.

Listing 10.26. Posting a Note for a User with the Social Data Service


// PostNote - Posts a note for the user
internal void PostNote()
{
    BasicHttpBinding httpBinding = new BasicHttpBinding();
    EndpointAddress endpoint =
        new EndpointAddress(this.socialEndpointAddress);

    SocialDataServiceSoapClient socialClient =
        new SocialDataServiceSoapClient(httpBinding, endpoint);
    socialClient.AddCommentCompleted += new
        EventHandler<AddCommentCompletedEventArgs>((s, e) =>
        {
            this.NoteToPost = "";
        });
    socialClient.Endpoint.Behaviors.Add(new AsmxBehavior());
    socialClient.AddCommentAsync(this.userUrl, this.NoteToPost, false,
        this.PreferredName);
}


Figure 10.8 shows the note, posted back in Figure 10.2, which shows up in the user’s note board on her My Site. The posting also shows up in the originating user’s activity feed.

Figure 10.8. The note sent in Figure 10.2 is delivered.

image

Updating SearchView for Silverlight 5

Silverlight 5 adds a great feature that enhances the SearchView solution while simplifying its code.

The Silverlight 4 version of the solution contains two ListBox controls on its main page, one for people and one for “all items” (generic documents). Silverlight 5 includes implicit data templates, previously found only in WPF. Implicit data templates allow you to bind a control to a collection that contains multiple types of objects. During data binding, the data template is selected based on the type of each object. This not only removes the need for two ListBox controls, but it also allows for a mix of person and document results in the same display, as shown in Figure 10.9.

Figure 10.9. A Mixture of people and document results with Silverlight 5

image

The first step to making this work is to introduce different kinds of ResultsItem objects so implicit data binding knows which template to use. In the SearchViewSL5 project, ResultsItem.cs has been updated as shown in Listing 10.27 to include subclasses for general or people results.

Listing 10.27. Creating Subclasses of ResultsItem


// Base class for managing collections of results
public class ResultsItem
{
}

// ResultsItem with all possible fields - for retrieval from XML
public class ResultsItemAll : ResultsItem
{
    public string Title { get; set; }
    public string Url { get; set; }
    public string UserName { get; set; }
    public string Summary { get; set; }
    public string Date { get; set; }
    public string Size { get; set; }
    public string JobTitle { get; set; }
    public string Department { get; set; }
    public string WorkPhone { get; set; }
    public string Email { get; set; }
    public string EmailUrl { get; set; }
    public string PictureUrl { get; set; }
}

// Results Item with general fields - for binding to a general result
public class ResultsItemGeneral : ResultsItem
{
    public string Title { get; set; }
    public string Url { get; set; }
    public string UserName { get; set; }
    public string Summary { get; set; }
    public string Date { get; set; }
    public string Size { get; set; }
}

// Results Item with Person fields - for binding to a Person result
public class ResultsItemPerson : ResultsItem
{
    public string Title { get; set; }
    public string Url { get; set; }
    public string UserName { get; set; }
    public string Summary { get; set; }
    public string Date { get; set; }
    public string Size { get; set; }
    public string JobTitle { get; set; }
    public string Department { get; set; }
    public string WorkPhone { get; set; }
    public string Email { get; set; }
    public string EmailUrl { get; set; }
    public string PictureUrl { get; set; }
}


In addition to ResultsItemGeneral and ResultsItemPerson, a third subclass called ResultsItemAll is defined with all possible properties present; this is used when parsing the XML when it’s too early to tell if an item will be a ResultsItemGeneral or a ResultsItemPerson.

Next, SearchService.cs needs to be smart enough to separate the people and general results into the two subclases. This is accomplished by introducing a function, getResultsItem(), which is used in the select clause of the LINQ query. The function is shown in Listing 10.28, and the updated LINQ query from the queryClient_QueryCompleted event handler is shown in Listing 10.29.

Listing 10.28. Function to Sort Search Result Items into Two Subclasses


private ResultsItem getResultsItem(ResultsItemAll originalResult)
{
    ResultsItem finalResult = null;
    if (originalResult.UserName == "")
    {
        ResultsItemGeneral r = new ResultsItemGeneral();
        r.Title = originalResult.Title;
        r.Date = originalResult.Date;
        r.Size = originalResult.Size;
        r.Summary = originalResult.Summary;
        r.Url = originalResult.Url;
        r.UserName = originalResult.UserName;

        finalResult = r;
    }
    else
    {
        ResultsItemPerson r = new ResultsItemPerson();
        r.Title = originalResult.Title;
        r.Date = originalResult.Date;
        r.Size = originalResult.Size;
        r.Summary = originalResult.Summary;
        r.Url = originalResult.Url;
        r.UserName = originalResult.UserName;
        // Person-specific properties:
        r.Department = originalResult.Department;
        r.Email = originalResult.Email;
        r.EmailUrl = originalResult.EmailUrl;
        r.JobTitle = originalResult.JobTitle;
        r.PictureUrl = originalResult.PictureUrl;
        r.WorkPhone = originalResult.WorkPhone;

        finalResult = r;
    }

    return finalResult;
}


Listing 10.29. Updated LINQ Query to Call getResultsItem()


// Project the results into a collection of result items
IEnumerable<ResultsItem> resultsItems =
          from r in resultsXml.Descendants(srd + "Document")
          select getResultsItem (new ResultsItemAll
          {
              Title = getPropertyValue(r, "Title"),
              UserName = getPropertyValue(r, "AccountName"),
              Url = getPropertyValue(r, "Path"),
              Summary = getSummary(r),
              Date = convertDate(getPropertyValue(r, "Write")),
              Size = convertSize(getPropertyValue(r, "Size")),
              JobTitle = getPropertyValue(r, "JobTitle"),
              Department = getPropertyValue(r, "Department"),
              WorkPhone = getPropertyValue(r,"WorkPhone"),
              Email = getPropertyValue(r,"WorkEmail"),
              EmailUrl = "Mailto:" + getPropertyValue(r, "WorkEmail"),
              PictureUrl =
                 convertPictureUrl(getPropertyValue(r, "PictureURL"))
          });


The result of all this is that the SearchResults property in MainPageVM.cs will be a collection with a mix of PeopleResultsItem objects and GeneralResultsItem objects. The two ListBox controls in MainPage.xaml can then be replaced with the single ListBox control shown in Listing 10.30. The ListBox.Resources element contains two DataTemplate elements—one for people and one for general results.

Listing 10.30. Updated Results ListBox


<!-- Results List Box shows People and General Results -->
<!--  using Silverlight 5 Implicit Data Binding        -->
<ListBox ItemsSource="{Binding Path=SearchResults}"
         SelectedItem="{Binding Path=SelectedResult, Mode=TwoWay}">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="MouseLeftButtonUp">
      <ei:CallMethodAction MethodName="ShowPersonDetail"
                           TargetObject="{Binding}" />
    </i:EventTrigger>
  </i:Interaction.Triggers>
  <ListBox.Resources>
    <DataTemplate DataType="local:ResultsItemPerson">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="60" />
          <ColumnDefinition Width="200" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Image Grid.Column="0" Height="55" Width="55"
               Source="{Binding PictureUrl}" Margin="5,2"/>
        <StackPanel Grid.Column="1" Margin="5,2">
          <TextBlock Text="{Binding Title}" FontSize="14"
                     FontWeight="Bold" />
          <HyperlinkButton NavigateUri="{Binding EmailUrl}" FontSize="12"
                     TargetName="_blank" Content="{Binding Email}" />
          <TextBlock Text="{Binding WorkPhone}" FontSize="12" />
        </StackPanel>
        <StackPanel Grid.Column="2" Margin="5,2">
          <TextBlock Text="{Binding JobTitle}" FontSize="12" />
          <TextBlock Text="{Binding Department}" FontSize="12" />
          <HyperlinkButton NavigateUri="{Binding Url}" FontSize="12"
                     TargetName="_blank" Content="My Site" />
        </StackPanel>
      </Grid>
    </DataTemplate>
    <DataTemplate DataType="local:ResultsItemGeneral">
      <StackPanel Orientation="Vertical">
        <StackPanel Margin="5,0,0,0" Orientation="Horizontal">
          <HyperlinkButton NavigateUri="{Binding Url}"
                     Content="{Binding Title}" FontSize="14"
                     FontWeight="Bold" />
          <TextBlock Text="{Binding Date}" FontSize="12"
                     FontStyle="Italic" Margin="5,0,0,0" />
          <TextBlock Text="{Binding Size}" FontSize="12"
                     FontStyle="Italic" Margin="5,0,0,0" />
        </StackPanel>
        <TextBlock Margin="5,0,0,2" Text="{Binding Summary}"
                   FontSize="12" TextWrapping="Wrap" Width="500" />
      </StackPanel>
    </DataTemplate>

  </ListBox.Resources>
</ListBox>


Building Custom WCF Services for SharePoint

As great as all the SharePoint client-side APIs and web services might be, there inevitably comes a time when you want to do something on the SharePoint server that isn’t available in any of them. At that point, the only option might be to write your own web service and deploy it to SharePoint. Fortunately, Visual Studio 2010 and SharePoint 2010 make it pretty easy to do this using Windows Communications Foundation (WCF).

There are a few reasons to write a custom WCF service for SharePoint:

• To access APIs and resources on the server that are not available via client APIs or web services

• To elevate privileges or run under a server-based security context

• To cache information on the server

• To consolidate a series of operations into a single round-trip

The Chapter10b solution in this chapter’s code download illustrates the first three of these points. It renders a simple storage meter showing the amount of storage for the current site collection relative to its quota, as shown in Figure 10.10. The quota information is not present in the client-side Site object. Further, accessing this information requires site collection administrative privileges. Finally, the information changes infrequently and is a good candidate for caching on the server rather than looking it up each time.

Figure 10.10. Web part to display the site collection storage and quota

image

Creating a Custom Web Service

To begin, create SharePoint and Silverlight projects as usual. This time you need to make it a Farm solution because you need to deploy some files on the web servers and this is not permitted in a sandboxed solution.

Next, add a WCF Service Library project. This project won’t actually be used, but it provides an easy way to generate the framework for our WCF service. Copy the Service1.cs and IService1.cs files into your SharePoint project and then delete the WCF Service Library project.

Next, add references to the SharePoint project for System.Runtime.Serialization and System.ServiceModel. These are needed to run the WCF service. You also need to add a reference to the SharePoint Client Server Runtime, which requires you to browse into the Global Assembly Cache. On the Add Reference dialog box, click the Browse tab and navigate to your Windows directory, and then assemblyGAC_MSILMicrosoft. SharePoint.Client.ServerRuntime. There is another directory inside and within that, the DLL to reference—Microsoft.SharePoint.Client.ServerRuntime.dll.

The code that runs in your web service is referenced in a .svc file, which must be deployed to the ISAPI directory; remember, this ends up appearing in the _vti_bin folder under every site in the SharePoint farm. The easiest way to get it there is to use a mapped folder, which maps a Visual Studio folder to a folder in the SharePoint installation directory. To create a mapped folder, right-click the SharePoint project and click Add, and in the fly-out menu select SharePoint Mapped Folder.... A dialog box appears showing you the SharePoint installation directory (or SharePoint Root) structure. Click the ISAPI folder and click OK, and you see an ISAPI folder in your Visual Studio project. Anything placed in that folder will be installed into the corresponding folder on all SharePoint servers when the solution is installed.

Create a text file in the ISAPI mapped folder and name it for your service, ending in .svc. Enter the following markup into the file, substituting your web service object and assembly name (these are for the sample project):

<%@ServiceHost Language="C#" Debug="true"
  Service="Chapter10b.TestService, Chapter10b, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=6795e15e830bf8f4"
  Factory="Microsoft.SharePoint.Client.Services.MultipleBaseAddressBa-
sicHttpBindingServiceHostFactory, Microsoft.SharePoint.Client.ServerRuntime,
Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

The Service attribute tells SharePoint where to find your web service code, which is installed along with the solution. The Factory attribute should be copied as-is, all on one line, and tells WCF to use SharePoint’s service host factory to host the service.


Tip

Entering an application’s full name is a cumbersome process and can be error-prone when application versions and signatures change. SharePoint’s configuration files (and WCF’s configuration file in this case) frequently need this information, so Visual Studio 2010 has added a feature to make it easier. Instead of the full application name, the token $SharePoint.Project.AssemblyFullName$ can be used, and Visual Studio substitutes the application name, such as Chapter10b.TestService, Chapter10b, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6795e15e830bf8f4 at compile time.

The tricky part is that by default, Visual Studio only does this for known SharePoint configuration files, so if you want to do it in your .svc file, you need to tell Visual Studio. To do this, right-click the SharePoint project and select Unload Project. All the project items disappear. Right-click the project again, which is now marked unavailable, and select Edit. This enables you to edit the .csproj file directly.

Add this line to the first <PropertyGroup> element to enable token substitution in .svc files:

<TokenReplacementFileExtensions>svc</TokenReplacementFile
Extensions>

Save your changes and right-click the project again, this time selecting the Reload Project option. Your project now provides SharePoint token replacement in .svc files.


At this point, you can rename and add your own code to Service1.cs and IService1.cs. The code sample provides a web service called TestService.svc, which has a service contract defined in ITestService.cs and is implemented in TestService.cs. The service is simple and defines a single method, GetUsageData(), which returns a small class containing the current site collection storage and quota. Listing 10.31 shows the ITestService.cs service contract.

Listing 10.31. ITestService Service Contract


[ServiceContract]
public interface ITestService
{
    [OperationContract]
    UsageData GetUsageData();
}

[DataContract]
public class UsageData
{
    [DataMember]
    public long Storage { get; set; }
    [DataMember]
    public long StorageWarningLevel { get; set; }
    [DataMember]
    public long StorageMaximumLevel { get; set; }
}


The TestService implementation is in TestService.cs and derives from the service contract interface ITestService. The code is shown in Listing 10.32.

Listing 10.32. TestService Implementation


[BasicHttpBindingServiceMetadataExchangeEndpoint]
[AspNetCompatibilityRequirements
 (RequirementsMode=AspNetCompatibilityRequirementsMode.Required)]
public class TestService : ITestService
{
    public UsageData GetUsageData()
    {
        SPSite site = SPContext.Current.Site;
        // UsageData is defined in ITestService.cs
        UsageData result = HttpRuntime.Cache["USAGE_DATA_" +
            site.ServerRelativeUrl] as UsageData;

        if (result == null)
        {
            result = new UsageData();

            SPSecurity.RunWithElevatedPrivileges(() =>
            {
                using (SPSite elevatedSite = new SPSite(site.ID))
                {
                    result.Storage = elevatedSite.Usage.Storage;
                    result.StorageWarningLevel =
                        elevatedSite.Quota.StorageWarningLevel;
                    result.StorageMaximumLevel =
                        elevatedSite.Quota.StorageMaximumLevel;
                }
            });

            HttpRuntime.Cache.Add
                ("USAGE_DATA_" + site.ServerRelativeUrl,
                 result, null, DateTime.Now.AddMinutes(10),
                 TimeSpan.Zero, CacheItemPriority.Default, null);
        }
        return result;
    }
}


The GetUsageData() method begins by accessing the current SharePoint site collection from SPContext.Current.Site; this is really just to get the site collection URL. The next step is to check the ASP.NET cache to see if the usage data is already in cache. The ASP.NET cache can be accessed as System.Web.HttpRuntime.Cache and is capable of caching any .NET object, in this case a UsageData object for the site. The cache key contains the site URL, as in USAGE_DATA_/sites/chapter10, to ensure it is unique to each site collection. If the cache returns a valid UsageData object, it is returned; if it is null, the new UsageData object must be created.

The usage and quota data are readily available as properties of the SPSite object, so it would seem simple enough to just load them into the new UsageData object and get on with it. However, accessing these properties requires administrative privileges, so if you want the web part to work for everyone, you need to run under elevated privileges. SharePoint’s SPSecurity class provides a static method, RunWithElevatedPrivileges, which runs any delegate with the equivalent privileges of a Farm administrator. Needless to say, this API should be used with care.

In this case, the delegate that RunWithElevatedPrivileges will run is an anonymous function declared inline in the code. Notice that the first thing it does is to create a new SPSite object! If the site variable had been used, where SPSite was obtained from the (unelevated) SharePoint context, it would still run under the permissions of the calling user. By creating a new SPSite object and using the URL of the old one, you get a fully elevated instance of the object and can access all its members.

Finally, the result is added to the ASP.NET cache with a lifetime of ten minutes, so if another request is made within that time, the server processing will be minimal.

Consuming the Custom Web Service

Consuming a custom WCF service is very similar to the .asmx examples earlier in the chapter. Ensure the WCF service is configured correctly before attempting to add a service reference and then add a reference to the .svc file’s URL, adding “/MEX” to the end, as in http://myserver/sites/testsite/_vti_bin/TestService.svc/MEX. The /MEX specifies WCF’s Metadata Exchange Format.

The sample solution includes a simple Silverlight web part that consumes the test service and displays the site’s storage and quota on a Silverlight progress bar. If no quota is set, an arbitrary amount is chosen to control the size of the progress bar. Listing 10.33 shows the Silverlight code, which calls the service and updates the progress bar; in the download, this code is in MainPage.xaml.cs in the TestServiceSL project.

Listing 10.33. Calling the Custom Web Service


BasicHttpBinding binding = new BasicHttpBinding();
EndpointAddress endpoint = new EndpointAddress(endpointAddress);

TestServiceClient tsClient = new TestServiceClient(binding, endpoint);
tsClient.GetUsageDataCompleted += ((s1, e1) =>
{
    UsageData usage = e1.Result as UsageData;
    if (usage != null)
    {
        if (usage.StorageMaximumLevel > 0)
        {
            // Calculate percentage use (progress bar is set for 0-100)
            storageProgressBar.Value =
                100 * usage.Storage / usage.StorageMaximumLevel;
            storageTextBox.Text =
                usage.Storage.ToString("N0") +
                " bytes of a maximum of " +
                 usage.StorageMaximumLevel.ToString("N0");
        }
        else
        {
            // If no quota is set, use 250MB and simulate a percentage
            storageProgressBar.Value = 100 * usage.Storage / 250000000;
            storageTextBox.Text = usage.Storage.ToString("N0") +
                " bytes";
        }
    }
});

// Call the service
tsClient.GetUsageDataAsync();


The sky’s the limit with the ability to create your own web services having full access to the SharePoint servers. It is extra work, however, and requires a Farm solution, so it’s still better to use SharePoint’s out-of-the-box Client Object Model and web services when possible.

Summary

SharePoint 2010’s Client Object Model and RESTful list services provide the easiest way to get at SharePoint site content, but there’s a lot more to SharePoint than just the site content. SharePoint provides a variety of web services to access its many services, and these can be used in your Silverlight solutions. When all else fails, it’s also possible to write and deploy your own web services to SharePoint as a Farm solution.

Given all these possibilities, there’s no corner of SharePoint so dark that your Silverlight application can’t make use of it!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.106.237