Using Client Logic to Reduce Server Load

When you create an application in client-side code, you often need to consider alternative ways of implementing your data operations in order to maximize efficiency. For example, in the SharePoint List Data Models reference implementation, we used a view to display a list of parts together with their inventory locations. Now suppose that you want to implement a similar interface using client-side logic. Parts and inventory locations are stored in two different lists. Since parts can be in more than one inventory location, there may be more than one entry for a part. In other cases a part may be out of stock, and therefore not have any inventory locations. In the SharePoint List Data Models RI, we used a left outer join between the Parts list and the Inventory Locations list. However REST doesn’t support this approach. To replicate this user interface on the client using REST, we would need to submit multiple queries and merge the results.

In the Silverlight REST Alt and Silverlight REST Alt No MVVM interfaces, we demonstrate a more efficient alternative approach for client-side logic that meets the same overall requirements. Instead of merging parts data with inventory locations data, we initially simply retrieve a list of parts. When the user selects a part, we retrieve the inventory locations associated with that part. In this case, the application uses the asynchronous communication model associated with RIA technologies to its advantage. The service only retrieves the specific inventory locations data it requires, so the actual request is small and fast. Since the client works asynchronously, the interface remains responsive while the data is retrieved. Clearly this approach would be less desirable if we used a traditional thin client model that required a full page postback to retrieve the inventory locations data.

In the remainder of this topic, we walk through this approach for the Silverlight REST Alt No MVVM interface. The Silverlight REST Alt interface demonstrates the same approach using the Model-View-ViewModel (MVVM) pattern, which we discuss in the next topic.

Our user interface is provided by the MainPage.xaml Silverlight control. The following image shows a screen capture of the MainPage.xaml control in design view, with the main data-bound components labeled.

The MainPage.xaml control

Ff798367.ff48f412-a970-410d-b560-4606b11fcd72(en-us,PandP.10).png

PartsDataGrid and SuppliersGrid are standard Silverlight DataGrid controls. The partsLocation1 control is a custom user control that is displayed as a dialog when required. When the MainPage control is loaded, the constructor performs some initialization tasks, including creating a data context object that will be used in all subsequent interactions. This illustrates one of the key differences between traditional thin client approaches and newer RIA approaches—when you build a user interface using an RIA technology, you no longer have to recreate every item on each request. Instead, it is common to create and store objects that you will reuse over the lifetime of the page. This page lifetime is typically much longer than that of a traditional server-driven Web page. The following code shows the constructor for the MainPage control.

public partial class MainPage : UserControl
{
  private readonly string partsSiteURL = "/sites/sharepointlist/";
  private readonly string listService = "_vti_bin/listdata.svc";
  private PartsItem currentPart;
  private readonly ObservableCollection<PartsItem> parts = 
    new ObservableCollection<PartsItem>();
  private readonly ObservableCollection<SuppliersItem> currentPartSuppliers = 
    new ObservableCollection<SuppliersItem>();
        
  public MainPage()
  {
    InitializeComponent();
    Uri appSource = App.Current.Host.Source;
    string fullPartsSiteUrl = string.Format("{0}://{1}:{2}{3}{4}", 
      appSource.Scheme, appSource.Host, appSource.Port, partsSiteURL, 
        listService);
    
    this.DataContext = new PartsDataContext(new Uri(fullPartsSiteUrl));

    PartsDataGrid.ItemsSource = parts;
    SuppliersGrid.ItemsSource = currentPartSuppliers;
  }
  ...

The MainPage constructor takes the following actions:

  • It calls the InitializeComponent method. This is an automatically generated class, common to all Silverlight applications, that creates and initializes the controls defined in the corresponding MainPage.xaml file.
  • It builds the URL of the site that hosts the Silverlight application.
  • It instantiates a data context object of type PartsDataContext, using the site URL, and assigns it to the DataContext property of the MainPage control. The PartsDataContext class was generated by Windows® Communication Foundation (WCF) Data Services and contains strongly typed entities that represent lists and list items on our site. The partLocation1 user control contained within the MainPage user control will inherit this context.
  • It assigns the ItemsSource property of the PartsDataGrid and the SuppliersGrid controls to observable collections of PartsItem and SuppliersItem, respectively. PartsItem and SuppliersItem are entity classes, defined by the data context, that represent items in the Parts list and the Suppliers list.

An ObservableCollection implements two key interfaces that support dynamic data binding. The INotifyCollectionChanged interface specifies that the class provides notifications whenever the collection has changed. The INotifyPropertyChanged interface specifies that the class provides notifications, in the form of a PropertyChanged event, when an item within the collection is added, removed, or altered. As a result of this mechanism, when you bind an ObservableCollection to a DataGrid control, the user interface and the underlying collection will automatically remain synchronized.

To find parts, the user types some text into the search box and then clicks Search. In the MainPage code-behind class, the PartSearchButton_Click method handles this event. The event handler calls the GetParts method, which builds a query and submits it to the REST interface.

private void PartSearchButton_Click(object sender, RoutedEventArgs e)
{
  GetParts(PartSkuTextBox.Text);
  partLocations1.CurrentLocation = null;
  partLocations1.ResetPart();
}

public void GetParts(string Sku)
{
  parts.Clear();
  var context = (PartsDataContext)this.DataContext;
  //Define Query
  var query = (DataServiceQuery<PartsItem>)context.Parts
                .Where(p => p.SKU.StartsWith(Sku))
                .Select(p => new PartsItem
                {
                  Title = p.Title,
                  SKU = p.SKU,
                  Id = p.Id,
                  Description = p.Description
                });

  //Execute Query
  query.BeginExecute(DisplayParts, query);
}

The key points to note in this example are as follows:

  • The query uses the same data context object that we created in the constructor.
  • View projection is used to select only four of the available fields for each PartsItem entity. View projections select a subset of available fields in the query, which reduces bandwidth consumption and processing overhead. However, any PartsItem properties that were not retrieved will have invalid values.
  • The BeginExecute method is called on the query, which causes the query to be executed asynchronously. The DisplayParts delegate will be invoked once the query has completed.

At this point, control is returned to the user interface while the data is being retrieved, which means that the user interface remains responsive. When the query results are returned, the DisplayParts method is invoked.

private void DisplayParts(IAsyncResult result)
{
  Dispatcher.BeginInvoke(() =>
  {
    DataServiceQuery<PartsItem> query = 
      (DataServiceQuery<PartsItem>)result.AsyncState;
    
    var partResults = query.EndExecute(result);
    foreach (var part in partResults)
    {
      parts.Add(part);
    }
  });
}

In this method, the call to Dispatcher.BeginInvoke uses a lambda expression to execute an anonymous method asynchronously on the UI thread. Dispatcher.BeginInvoke ensures that the logic executes on the same thread as the UI, which is compulsory for logic that interacts with the UI in all Silverlight applications. If you don’t take this action, you will receive an exception or a cross-threading violation. Because we maintain our parts list in an ObservableCollection, we simply need to update the collection. Any controls that are data-bound to the collection are updated automatically, as shown by the following image.

Part SKU search results

Ff798367.7a433f45-f650-438e-accd-7d8ffe07fd9d(en-us,PandP.10).png

When the user selects a row, the PartsDataGrid_SelectionChanged method handles the event. This method must perform two key actions—it must retrieve the suppliers for the selected part, and it must retrieve the locations of the selected part.

private void PartsDataGrid_SelectionChanged(object sender, 
                                            SelectionChangedEventArgs e)
{
  currentPart = PartsDataGrid.SelectedItem as PartsItem;
  GetPartSuppliers();
  partLocations1.GetLocations(currentPart.Id);
}

In this topic we focus on retrieving the part locations. The event handler calls the GetLocations method on the partLocations1 control, passing in the part ID from the currentPart local variable as an argument. This is shown by the following code example.

public void GetLocations(int partId)
{
  var context = (PartsDataContext)this.DataContext;
  inventoryLocations.Clear();

  this.currentPartId = partId;
  var query = (DataServiceQuery<InventoryLocationsItem>)context.InventoryLocations
                .Where(p => p.PartId == partId)
                .Select(p => new InventoryLocationsItem 
                {   BinNumber = p.BinNumber, 
                  Id = p.Id, 
                  Quantity = p.Quantity, 
                  Title = p.Title, 
                  PartId = p.PartId 
                });

  //Execute Query
  query.BeginExecute(DisplayLocations, query);
}

As before, the callback method—DisplayLocations in this case—uses a call to Dispatcher.BeginInvoke to update an observable collection. The inventoryLocations local variable is an observable collection of type InventoryLocationsItem.

private void DisplayLocations(IAsyncResult asyncResult)
{
  Dispatcher.BeginInvoke(() =>
  {
    DataServiceQuery<InventoryLocationsItem> query =
      (DataServiceQuery<InventoryLocationsItem>)asyncResult.AsyncState;

      var partLocations = query.EndExecute(asyncResult);
      foreach (var location in partLocations)
      {
        inventoryLocations.Add(location);
      }
  });
}

Because the inventoryLocations observable collection is data-bound to the locationsDataGrid control, the user interface automatically updates to show the locations associated with the selected part.

Location data for selected part

Ff798367.05800972-725c-4da9-adf0-81717f007103(en-us,PandP.10).png

As you can see, this approach provides an efficient, responsive user interface without increasing server load or performing heavy duty processing on the client.