Locations

Blog Stats

Posts Tagged ‘ObservableCollection’

This post presents a solution to a question on Stack Overflow which I found interesting, this is a question I’ve seen crop up a few times now in one form or another:

"I have an ObservableCollection feeding a DataGrid that is updating nicely. The point: I want to filter (collapse) the rows without removing them from the collection. Is there a way to do this?"

You can remove rows from a DataGrid by removing items from the ObservableCollection<T> that the grid’s ItemsSource property is bound to, but the question asks is there a way to avoid this. The way I see it a solution to this question will firstly offer a way to filter the rows displayed in a DataGrid without affecting the Count of the original ObservableCollection<T> and secondly provide a way to easily discern which rows are currently filtered and which are displayed after the filter has been applied. The intent is also to provide a solution general enough to be reused with any type T in an ObservableCollection<T>.

The solution structure of the simple demo solution available for download above looks as follows:

The Xaml contains two DataGrids, a NumericUpDown control from the Silverlight Toolkit and a standard Button control. When the ‘Apply Filter’ button is clicked the people (or rows) shown in the FilteredPeople grid are filtered, the row count in the AllPeople grid remains unchanged. The filter excludes people whose Age property does not equal the current value of the NumericUpDown control.

The Page class has an ObservableCollection<Person> called ‘people’ containing 6 instances of the Person class. The Person class is a simple class that implements the INotifyPropertyChanged interface and has properties called FirstName, Age and IsVisible.

When an instance of the Page UserControl is loaded the ItemsSource property of both DataGrids is bound to the people collection resulting in the screenshot shown earlier.

When the ‘Apply Filter’ button is clicked we store the current value of the NumericUpDown control in the selectedAge variable and set the ItemsSource of the FilteredPeople grid to the result of calling the ApplyMutateFilter<T> extension method:

This method takes one predicate and two actions. A ‘predicate’ in this case just means a function that takes a parameter of type TSource and returns true or false. Similarly an ‘action’ is just a function that takes a parameter of type TSource and returns void.

In the call to ApplyMutateFilter<T> in the event handler for the button’s Click event above, the predicate function is designed to return the result of the comparison between a Person object’s Age property and the value of selectedAge. The two action functions are designed to set the value of the IsVisible property on a Person object to either true or false.

The ApplyMutateFilter<T> extension method body applies one of these actions to every item in the sequence, which action depends on the evaluation of the predicate. If the predicate returns true the positive action is executed otherwise the negative action is executed resulting in the value of the IsVisible property changing.

Finally only the elements in the sequence that matched the predicate are returned with the call to the Where<T> extension method, thus the filter is applied.

It’s important to remember that setting the IsVisible property does not make the row disappear from the DataGrid; it’s the call to Where<T> that filters the sequence. The IsVisible property is merely a convenient way of later finding which elements have been affected by the current filter in code, for example if you wanted to know what rows are currently being shown in the DataGrid after a filter has been applied.

Here is a screen shot after clicking the ‘Apply Filter’ button with a selected age of 21:

The FilteredPeople DataGrid now only displays people aged 21. The grid is now bound to the filtered sequence returned from the call to ApplyMutateFilter<T> with a predicate comparing each Person object’s Age property to the selectedAge of 21.

The screenshot shows the rows have been filtered in the first DataGrid without affecting the Count of the original ObservableCollection<T> which the second DataGrid’s is still bound to, thus satisfying the first task we set out to achieve.

Finally it’s clear to see the second requirement has been fulfilled from the state of the IsVisible properties. The value of this property makes it easy to discern which rows in the original ObservableCollection<T> are currently filtered and which are displayed after the filter has been applied.

Altering the predicate is all that’s required to apply a filter to a different property/column or a combination of several properties/columns.

Part 1 of this post showed a simple Silverlight application that populates a DataGrid with sample financial stock information read from an XML file extracted from a .xap package using LINQ to XML.

This post shows how to get the same stock information from an XML file and bind it to a DataGrid, but this time the data does not come from the client deployment package but instead from a WCF web service.

Here’s a look at the Visual Studio 2008 solution for Part 2 :

Now that the data is coming from the server, the Stocks.xml file containing 1000 sample stocks is located under the App_Data folder as part of the web site.

Also added to the web site is the WCF web service ‘StockService’. Support for WCF web services is provided in the System.ServiceModel namespace new to Silverlight 2, for a good introduction to WCF in Silverlight 2 have a look here, be sure to note the required change from wsHttpBinding to basicHttpBinding for Beta 1.

StockService implements the GetStocks() method and returns a generic list of Stock objects using the XmlFileStockService class that remains unchanged. In order to get the path to open the XML file from within the WCF service the HostingEnvironment.ApplicationPhysicalPath property is used. In ASMX services the same goal could be achieved using HttpContext.Current.Server.MapPath, however WCF services are host agnostic so unless your web site is running in aspnetCompatibilityMode the HttpContext will be null.

The Stock type is able to be returned and consumed from GetStocks() because it has been marked with the [DataContract] attribute, making it serializable by a serializer such as the DataContractSerializer :

If these attributes are removed and you try to ‘Add a Service Reference’ to the StockService web service from the Silverlight client application an exception is thrown with the message : ‘Metadata contains a reference that cannot be resolved’.

Our Silverlight client application is manifest by the StockListDemo.App project. ‘Add Service Reference Support’ and Web Services Client functionality is again new to Silverlight 2 and this project takes advantage of that by adding a Service Reference to our WCF StockService called WcfStockService, this is also the final part of a new namespace for the generated proxy in Reference.cs. You can view Reference.cs by clicking on ‘Show All Files’ when the WcfWebService service reference is selected, some of the generated code is worth a look, notice for example that the Stock class implements the INotifyPropertyChanged interface.

The UI for the Silverlight client is still defined in Page.xaml and still consists mainly of a DataGrid control. We are no longer relying on setting AutoGenerateColumns="True" due to a bug in Beta 1. If you specify AutoGenerateColumns="true" at present and then bind to a collection with a Count of 0 and exception is raised with the message "Enumeration has either not started or has already finished", to avoid this exception we define our own columns of type DataGridTemplateColumn as follows :

With the web service in place and the UI designed, all that remains is the code in Page.xaml.cs and StockTicker.cs that calls the web service and populates the UI with the results, firstly Page.xaml.cs :

Notice the order in which things are happening here : we are binding the DataGrid to the empty StockList collection property first, then forwarding an asynchronous call to the webservice using the GetStocksAsync() method of the stockTicker field. Because the collection the DataGrid is binding to is an ObservableCollection<WcfStockService.Stock>, the DataGrid will populate itself when this collection notifies that it has changed. Download the entire project using the link at the start to see this in action. The code for the StockTicker class is below:

The StockList collection is initialised when the class is instantiated and the property is populated in the handler for the web service’s GetStocksCompleted event. In order to use the ForEach(Action<T> action) generic method to populate the collection, the Collection Type for the proxy generation code has been changed from the default of Array to System.Collections.Generic.List:

The handler for the GetStocksCompleted event is wired up in the GetStocksAsync() method along with the creation of objects for the webservice binding and endpoint. The endpoint address is not hardcoded but instead retrieved based on the Uri of the current HtmlPage in the custom Utils.GetHostEndpointAddress method. Also notice that we are setting the MaxReceivedMessageSize property of our binding object, in fact this is the whole reason we are creating a Binding object and an EndpointAdress object and using them in the creation of the StockServiceClient instead of just calling the service’s default parameter-less constructor. The MaxReceivedMessageSize property is initially set to 65536 (64 kilobytes), if you try to return more data than this from a WCF web service without increasing this limit at present, a QuotaExceededException is thrown with the message MaxReceivedMessageSizeExceeded :

For more on this have a look here, especially the second post. The Stocks.xml file contains 1000 stock objects after all so to enable all this data to be returned from the service the MaxReceivedMessageSize property is set to int.MaxValue although it doesn’t need to be set that high for production where DOS issues could be a concern. Interestingly this property is actually of type long or Int64 but if you try to set it to long.MaxValue for example, an ArgumentException is thrown with the message MaxReceivedMessageSizeMustBeInIntegerRange. As a last word on the MaxReceivedMessageSize property, notice that there is a setting in the ServiceReferences.ClientConfig file generated by adding a Service Reference :

There is a known bug that changing these values has no effect in Beta 1, you have to change the property in code as shown above, this will obviously be resolved at some point in a future release.

Now the Silverlight UI is wired up to the WCF service via the StockTicker and the results appear in the DataGrid things are looking good. To prove that the INotifyPropertyChanged interface on the generated Stock class and the ObservableCollection of those Stocks is working as expected, a textbox and button have been added to allow modification of the DataGrid SelectedItem’s Symbol name. Notice that the code in the handler for the button’s Click event merely changes the value of the selected Stock’s Symbol property and the DataGrid updates automatically. This is purely for educational value of course, normally a symbol name would not change :

To progress with our sample financial RIA stock ticker application the next area to look at is updating the Stock prices, to do this we could call the web-service at a specified interval, but this is not ideal. What we really want is not to have to ‘pull’ updates from the server but have the server ‘push’ updates to the listening client. There is support for this type of duplex communication with the application’s host of origin in Silverlight 2 Beta 1 provided by the Sockets class in the System.Net networking namespace. Depending on feedback this will be the subject of a future post.