As you've seen, you populate the DataGrid
and ListBox
controls in exactly the same manner as one another, by assigning/binding a collection or collection view to their ItemsSource
properties.
Let's now look into this topic further, and investigate the type of data that these controls will accept. We'll introduce the ObservableCollection<T>
type, and then introduce the concept of collection views that will allow us to manipulate the data displayed in the summary list without affecting the underlying collection. We'll then demonstrate how you can go about wiring these controls up to this data.
Note A key concept in Silverlight is that a view should “pull” data into itself, rather than have data “pushed” into it from the code-behind. Although Silverlight enables you to “push” data into a view, as a general rule and best practice, you should take the “pull” approach when populating a view with data, which is the sole approach that we'll take from now on. If you're used to “pushing” data into views, a common practice with technologies such as Windows Forms, this will require you to take a different mindset when you are designing your application, and can take a little getting used to. However, after you get the hang of it, you won't want to go back to your old ways.
Silverlight supports many of the common generic collection types you might be used to from the full .NET Framework, including List
, Dictionary
, LinkedList
, Stack
, and Queue
. Other nongeneric collections, such as the ArrayList
and Hashtable
, aren't implemented in Silverlight, but you can find an appropriate alternative among the generic collections.
However, the most important collection type that you find yourself using in Silverlight is the ObservableCollection<T>
collection. This is a generic collection type, unique to Silverlight and WPF, that implements the INotifyCollectionChanged
interface, which exposes a CollectionChanged
event—raised when items are added or removed from the collection.
This behavior is incredibly useful because Silverlight controls such as the ListBox
, DataGrid
, and ComboBox
listen for this event when their ItemsSource
property is bound to an ObservableCollection<T>
collection, and automatically update themselves when the collection has changed. This means that you can update the collection in code, and the changes will be automatically propagated to the user interface. So when you add an item to the collection, it will be automatically added to the control's items, and when you remove an item from the collection, it will be automatically removed from the control's items. This means that you don't need to write code to update the control when an item is added to or removed from the collection, and the code that is modifying this collection doesn't actually need to know that the collection is even bound to a control! Therefore, the ObservableCollection<T>
collection can help facilitate a clean separation of concerns between layers in your application, making it a key part of Silverlight's data binding ecosystem—particularly when you come to implement the MVVM (Model-View-ViewModel) design pattern, which we'll cover in Chapter 13.
Another important data binding concept related to collections is that of collection views. A collection view is a wrapper around a collection that allows the collection to be manipulated in the user interface, without altering the underlying collection. Think of it as a “view of a collection.” Typical manipulations you might want to perform include filtering, sorting, grouping, and paging the collection's items. For example, you might want to bind a collection to a ListBox
, and enable the user to dynamically filter the items displayed by entering some text into a TextBox
. Instead of adding/removing items from the bound collection, you can wrap the collection in a collection view, bind the control to this collection view instead of directly to the collection, and then simply apply a filter to the collection view. The collection view will expose only the items from the underlying collection that conform to the filter, and thus, the bound control will only display these items.
Another key feature of collection views is their current record pointer, which tracks the current item in the collection, enabling multiple controls bound to the same collection view to be kept synchronized. Collection views expose a number of methods and properties that you can use to move this pointer and navigate through the items in the collection. We'll investigate this behavior further in Chapter 11, staying focused on the filtering/sorting/grouping/paging features of collection views in this chapter.
Note Collection views all implement the ICollectionView
interface. Some also implement the IEditableCollectionView
interface, which allows the underlying collection to be edited via the collection view, and some also implement the IPagedCollectionView
interface, which provides paging capabilities to the collection view.
Like the ObservableCollection<T>
collection, collection views help facilitate a clean separation of concerns between layers in your application, and are particularly useful when you come to implement the MVVM design pattern, which we'll cover in Chapter 13.
Note Some controls have built-in behavior that can manipulate the output of collection views. A good example of this is the DataGrid
control. It allows you to sort its rows by clicking a column header, and it does this without modifying the source collection. It simply tells the collection view that wraps it how the items should be sorted. If the DataGrid
control is bound directly to a collection, it internally wraps it in a collection view, and binds to that instead. Therefore, the DataGrid
control can still manipulate the items without modifying the collection that it is bound to.
There are a number of different types of collection views in Silverlight, and the type you use will really depend on the given scenario. Let's take a look at the most common collection views that you will use, and the scenarios in which you might use them.
The ListCollectionView
/EnumerableCollectionView
collection views can filter, sort, and group items in their underlying collection in memory. (Note that they don't support paging of the data.) You can't instantiate these collection views directly (their constructors are marked internal), but they can be instantiated with the help of the CollectionViewSource
class.
The CollectionViewSource
class acts as a collection view “proxy,” which can be used to create a collection view, usually as a means to do so declaratively in XAML. Let's say you want to bind a control to a collection in XAML, but display a filtered/sorted/grouped “view” of that collection, without modifying the collection itself. Wrapping the collection in a collection view enables you to do this. The CollectionViewSource
class provides a means of declaratively wrapping a collection in a collection view in XAML, which you can then bind to.
You define the CollectionViewSource
as a resource, assign a collection to its Source
property, and it will provide you a corresponding collection view from its View
property as either a ListCollectionView
or an EnumerableCollectionView
, depending on the type of collection that is being wrapped, which you can then bind to. The CollectionViewSource
class allows you to declaratively specify how the items from the collection should be filtered, grouped, and sorted (but not paged), via properties such as Filter
, GroupDescriptions
, and SortDescriptions
.
Note You can also use the CollectionViewSource
class to create a collection view in code. However, as a general rule, it is primarily used to create collection views declaratively in XAML.
Use the ListCollectionView
/EnumerableCollectionView
collection views when you encounter one of the following situations:
Unlike the ListCollectionView
/EnumerableCollectionView
collection views, a PagedCollectionView
can be instantiated directly, and also supports paging of the data. A PagedCollectionView
is often used when you are following the MVVM design pattern, and the view model wants to have some control over the filtering/sorting/grouping/paging behavior of the collection view.
Note To use the PagedCollectionView
collection view, your project needs a reference to the System.Windows.Data.dll
assembly.
Use the PagedCollectionView
collection view when you encounter one of the following situations:
The DomainDataSourceView
collection view is exposed by the DomainDataSource
control via its DataView
property, and provides a view over the collection of entities requested from the server. You normally won't interact directly with this collection view, but it's worth knowing that it exists. You can't create an instance of a DomainDataSourceView
object in your code; only the DomainDataSource
control can create this collection view (the two are tightly coupled together). You can apply filtering, sorting, grouping, and paging criteria to a DomainDataSource
control, and this collection view will display the data accordingly is performed on the server.
The DomainCollectionView
collection view was introduced as part of the WCF RIA Services Toolkit to help make RIA Services more MVVM friendly. It essentially provides the same behavior as the DomainDataSource
control (loading of data via RIA Services, and server-side filtering, sorting, and paging of data), but in a manner that allows for a cleaner separation of concerns between your view and the domain context, such that the view does not need to know anything about how the data is obtained.
Note To use the DomainCollectionView
collection view, you need to have the WCF RIA Services Toolkit installed (discussed in Chapter 4, in the section “WCF RIA Services Toolkit”), and your project needs a reference to the Microsoft.Windows.Data.DomainServices.dll
assembly.
Whereas the CollectionViewSource
collection view proxy and PagedCollectionView
collection view manipulate the data in memory on the client, the DomainCollectionView
collection view is a little bit different in that it handles loading data from the server (via RIA Services), and actually manipulates the data on the server, in the same manner as the DomainDataSource
control. This is particularly useful when you're paging the data, as the server will return only the data for the current page, saving a lot of network traffic between the server and the client if there are many records on the server. When the client changes the filtering/sorting/grouping of the collection, or wants a new page of data, the DomainCollectionView
will request a new page of results from the server, and populates its source collection when the data is returned.
There are three key components involved when using the DomainCollectionView
. There's the DomainCollectionView
itself, the source collection that it wraps, and a “loader,” which is the class that actually handles the communication with the server and updates the source collection. The DomainCollectionView
acts as a bridge between the user interface and this loader. For example, say that the user interface wants a new page of data. The user interface will ask the DomainCollectionView
for the new page, which will then ask the loader for the new page, which will request the page of data from the server. When the response is received from the server, the loader will populate the source collection with that data, which gets fed back to the user interface via the DomainCollectionView
. The diagram in Figure 6-6 might help you understand the relationship between these components.
The WCF RIA Services Toolkit comes with a default loader implementation, named DomainCollectionViewLoader
. This loader merely passes the work of loading the data back to you. It takes method delegates as parameters to its constructor and calls those methods when data needs to be loaded or has been loaded, so it isn't particularly intelligent.
Note You can create your own custom loader if you want a smarter, less generic loader than the DomainCollectionViewLoader
. Kyle McClellan, from the RIA Services team, has information on how to do so on his blog here at http://blogs.msdn.com/b/kylemc/archive/2011/05/13/writing-a-custom-collectionviewloader-for-the-domaincollectionview-and-mvvm.aspx
, but we'll stick with the default loader implementation for now.
As the class DomainCollectionViewLoader
simply offloads the work of loading the data requested by the user interface back to you, it's up to you to write the logic to load the data from the domain context (demonstrated in Chapter 5). When creating the query to the server, you will need to apply the state of the DomainCollectionView
—that is, the sorting/grouping criteria and current page number—to the query. To help you with this, the CollectionViewExtensions
class, found in the WCF RIA Services Toolkit, contains some extension methods for the EntityQuery
class that can apply the state of the collection view to the query for you. We'll look at how you do this in the DomainCollectionView
approach section of the “Paging the Summary List” workshop.
Use the DomainCollectionView
collection view when you encounter one of the following scenarios:
In Chapter 5, you saw how you could easily wire up a DataGrid
control to a DomainDataSource
control and populate it with data exposed from the server via RIA Services by dragging an entity from the Data Sources tool window in Visual Studio. (See the section “Consuming Data Declaratively in XAML via the DomainDataSource Control” in Chapter 5.) In summary, you can bind a DataGrid
/ListBox
control to a DomainDataSource
control by binding the DataGrid
/ListBox
's ItemsSource
property to the DomainDataSource
control's Data
property using ElementName
binding, as follows:
<riaControls:DomainDataSource Name="productSummaryDDS"
AutoLoad="True"
QueryName="GetProductSummaryList"
LoadedData="productSummaryDDS_LoadedData">
<riaControls:DomainDataSource.DomainContext>
<my:ProductSummaryContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
<sdk:DataGrid ItemsSource="{Binding ElementName=productSummaryDDS, Path=Data}" />
Note Populating a view with data from the server by binding controls to a DomainDataSource
control is an easy way to prototype an application or get a simple application up and running quickly. However, when it comes to building robust applications, you are generally better off avoiding this approach and obtain data via the DomainCollectionView
collection view instead.
As a best practice, each view will have a “view model” class that will expose data to it, which the view can bind to. This is a key concept in the MVVM design pattern, which will be discussed in depth in Chapter 13. In the meantime, you should still start getting familiar with coding in this manner now, even if you don't understand all the MVVM design pattern concepts yet.
The process of creating a simple “view model” class and binding your view to it is really quite a straightforward process. Let's say you have a view named ProductListView
. Commonly, you'll have a corresponding view model class named ProductListViewModel
, which will contain all or most of the logic for the ProductListView
view. Most view logic should go into this class instead of being written in the code-behind. The view model class will expose data (via properties) and operations (generally via commands) to the view. An instance of the view model class can then be assigned to its DataContext
property, enabling controls in the view to bind to its properties.
Note Generally, your view model class will inherit from a base view model class that provides logic commonly implemented by view model classes. However, for now we're keeping things simple; we'll look at creating a base view model class in Chapter 13.
Now that you understand the basic concept of binding a view to a view model, let's look at how you do this in practice.
DataGrid
/ListBox
workshops earlier in this chapter, we (re)created a view named ProductListView.xaml
. If you haven't done one of these workshops, do so now, as we'll be using this view and the DataGrid
/ListBox
control that we configured to display our summary list.ProductListViewModel
in the same folder as the view (the Views
folder).
namespace AdventureWorks.Views
{
public class ProductListViewModel
{
}
}
using
statements to the top of the class's file:
using System.Collections.Generic;
using AdventureWorks.Web.Services;
using AdventureWorks.Web.Models;
ProductSummary
entities (which we created and exposed from a domain service named ProductSummaryService
in Chapter 4) from our view model, so add the following property to the class to do so:
public IEnumerable<ProductSummary> Products { get; set; }
ProductSummaryContext
domain context to get the complete list of ProductSummary
entities from the server. Create a constructor for the class that loads the data and assigns the results to the property, like so:
public ProductListViewModel()
{
var context = new ProductSummaryContext();
var qry = context.GetProductSummaryListQuery();
var op = context.Load(qry);
Products = op.Entities;
}
using System.Collections.Generic;
using AdventureWorks.Web.Services;
using AdventureWorks.Web.Models;
namespace AdventureWorks.Views
{
public class ProductListViewModel
{
public IEnumerable<ProductSummary> Products { get; set; }
public ProductListViewModel()
{
ProductSummaryContext context = new ProductSummaryContext();
var qry = context.GetProductSummaryListQuery();
var op = context.Load(qry);
Products = op.Entities;
}
}
}
Note The preceding example demonstrates using RIA Services to access data from the server, but it is equally applicable to whatever approach you might be using. Simply populate the collection with data using your chosen approach.
DataContext
property, as follows:
public ProductListView()
{
InitializeComponent();
this.DataContext = new ProductListViewModel();
}
Note You can actually instantiate the ProductListViewModel
class and assign it to the view's DataContext
property declaratively in XAML. For now, however, we'll do this in the code-behind, as doing so in XAML adds some complexity, such as checks for design-time/runtime mode, and so on. We look at how you can instantiate an object and assign it to the view's DataContext
property declaratively in XAML in Chapter 13.
ItemsSource
property of a DataGrid
control to the Products
property on the view model, like so:
<sdk:DataGrid ItemsSource="{Binding Products}" />
ProductListView.xaml
view. The summary list control will be populated with all the records from the server.As you've seen, wrapping a collection in a collection view allows you to display a filtered, sorted, grouped, and paged “view” of the collection without actually modifying the collection. In the previous workshop, the summary list consumed the collection directly, but let's now wrap it in a collection view so we can manipulate the display of the collection in the view. We'll look at how you wrap a collection using each of the collection views detailed earlier. (See those definitions for when you should use each type of collection view.)
As you previously saw, the CollectionViewSource
collection view proxy allows you to wrap a collection in a collection view declaratively in XAML. Let's use it to wrap the Products
collection exposed by our view model.
ProductListView
view's root element, like so:
<navigation:Page.Resources>
</navigation:Page.Resources>
CollectionViewSource
resource for the view, named productCollectionView
, and bind its Source
property to the Products
collection on our view model, as follows:
<navigation:Page.Resources>
<CollectionViewSource x:Key="productCollectionView" Source="{Binding Products}" />
</navigation:Page.Resources>
ItemsSource
property to this resource, like this:
<sdk:DataGrid ItemsSource="{Binding Source={StaticResource productCollectionView}}" />
Note You might have noted that the collection view is being exposed from the CollectionViewSource
class's View
property, but we're not explicitly binding to its View
property. This is because the binding engine recognizes that it is binding to a CollectionViewSource
, and assumes that it's not the CollectionViewSource
itself that you want to bind to, so it automatically drills down to its View
property and binds to that instead. The CollectionViewSource
essentially becomes transparent when binding to it, such that you are binding to the collection view that it exposes, rather than the CollectionViewSource
itself. You can turn off this View
property drilling behavior for the CollectionViewSource
by setting the BindsDirectlyToSource
property on the property bindings to True
.
To wrap a collection in a PagedCollectionView
, you will need to do so in code. Therefore, we'll need to wrap the collection in a PagedCollectionView
inside our view model, and expose it as a property from the view model to which the summary list can bind. Let's use it to wrap the Products
collection exposed by our view model.
System.Windows.Data.dll
assembly to your project.using
statement to the top of the ProductListViewModel
class's file:
using System.Windows.Data;
ProductListViewModel
class, named ProductCollectionView
, of type PagedCollectionView
, like so:
public PagedCollectionView ProductCollectionView { get; set; }
PagedCollectionView
in the view model's constructor, and assign the result to the ProductCollectionView
property, as follows:
ProductListView.xaml
view, we can now bind the summary list's ItemsSource
property to the ProductCollectionView
property on the view model, like this:
<sdk:DataGrid ItemsSource="{Binding ProductCollectionView}" />
Like the PagedCollectionView
, you will need to configure the DomainCollectionView
in code. We'll do this inside our view model, and expose it as a property from the view model to which the summary list can bind. Let's use it to expose a collection of ProductSummary
entities from the server from our view model.
Microsoft.Windows.Data.DomainServices.dll
to your Silverlight project.using
statements to the top of the ProductListViewModel
class's file:
using System.ServiceModel.DomainServices.Client;
using Microsoft.Windows.Data.DomainServices;
ProductListViewModel
class, named ProductCollectionView
, of type DomainCollectionView
, like so:
public DomainCollectionView ProductCollectionView { get; set; }
private ProductSummaryContext _context = new ProductSummaryContext();
Since filtering/sorting/grouping/paging the data in the DomainCollectionView
will each result in a request to the server, this will maintain a single instance of the domain context for use by each request.
private LoadOperation<ProductSummary> LoadProductSummaryList()
{
EntityQuery<ProductSummary> query = _context.GetProductSummaryListQuery();
return _context.Load(query);
}
private void OnLoadProductSummaryListCompleted(LoadOperation<ProductSummary> op)
{
if (op.HasError)
{
// NOTE: You should add some logic for handling errors here, and mark
// the error as handled.
// op.MarkErrorAsHandled();
}
else if (!op.IsCanceled)
{
((EntityList<ProductSummary>)Products).Source = op.Entities;
}
}
The LoadProductSummaryList
method performs the request to the server (via the domain context) for data, and the OnLoadProductSummaryListCompleted
method populates the source collection with the entities returned from the server. Note that the OnLoadProductSummaryListCompleted
method should contain some logic for handling errors (as per the comment), but for the purpose of this workshop, we'll leave exceptions unhandled.
public ProductListViewModel()
{
Products = new EntityList<ProductSummary>(_context.ProductSummaries);
var collectionViewLoader = new DomainCollectionViewLoader<ProductSummary>(
LoadProductSummaryList, OnLoadProductSummaryListCompleted);
ProductCollectionView =
new DomainCollectionView<ProductSummary>(collectionViewLoader, Products);
ProductCollectionView.Refresh();
}
The preceding code creates a new EntityList<ProductSummary>
collection and sets its backing entity set to that exposed from the domain context. It then instantiates the loader object, passing it delegates to the load and load completed methods we created in the previous step. We then instantiate the DomainCollectionView
object, passing its constructor the loader and the source collection, connecting the three together. Finally, we call the Refresh
method on the DomainCollectionView
, which will make it ask the loader to populate the source collection with data from the server.
ProductListView.xaml
view, we can now bind the summary list's ItemsSource
property to the ProductCollectionView
property on the view model, like so:
<sdk:DataGrid ItemsSource="{Binding ProductCollectionView}" />