Peter McGrattan’s Weblog

Silverlight, WCF, ASP.NET, AJAX, Graphics, RIA

Posts Tagged ‘DataGrid’

Filtering Silverlight DataGrid Rows with ApplyMutateFilter<T>

Posted by petermcg on January 29, 2009

(Code download)

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:

Solution Structure

The demo application running looks as follows:

App Running Pre-Filter

This UI is produced from the contents of Page.xaml:

<UserControl x:Class="DataGridRowFiltering.Page"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  
             xmlns:input="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls.Input">

    <StackPanel Margin="5,5,5,5" x:Name="LayoutRoot" Background="White">

        <StackPanel Orientation="Horizontal" Margin="0,15,0,15">
            <TextBlock Text="Age Filter" FontSize="16" VerticalAlignment="Center" TextAlignment="Center" Margin="0,0,10,0" />

            <input:NumericUpDown Width="40" x:Name="FilterValue" DecimalPlaces="0" Minimum="20.0" Maximum="25.0" Margin="0,0,10,0" />

            <Button x:Name="FilterButton" Content="Apply Filter" Click="FilterButton_Click" />
        </StackPanel>

        <StackPanel Margin="0,15,0,15">
            <TextBlock Text="Filtered People" FontSize="16" />
            <data:DataGrid x:Name="FilteredPeople"  AutoGenerateColumns="True" IsReadOnly="True" />
        </StackPanel>

        <StackPanel Margin="0,15,0,15">
            <TextBlock Text="All People" FontSize="16" />
            <data:DataGrid x:Name="AllPeople"  AutoGenerateColumns="True" IsReadOnly="True" />
        </StackPanel>

    </StackPanel>
</UserControl>

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 events for the UI are in Page.xaml.cs:

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace DataGridRowFiltering
{
    public partial class Page : UserControl
    {
        ObservableCollection<Person> people = new ObservableCollection<Person>()
        {
            new Person("Peter", 20),
            new Person("Bill", 21),
            new Person("Joe", 21),
            new Person("David", 23),
            new Person("Steve", 24),
            new Person("Jeff", 25),
        };

        public Page()
        {
            InitializeComponent();

            Loaded += Page_Loaded;
        }

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            FilteredPeople.ItemsSource = people;
            AllPeople.ItemsSource = people;
        }

        private void FilterButton_Click(object sender, RoutedEventArgs e)
        {
            int selectedAge = Convert.ToInt32(FilterValue.Value);

            FilteredPeople.ItemsSource = people.ApplyMutateFilter(p => p.Age == selectedAge,
                                                                  p => p.IsVisible = true,
                                                                  p => p.IsVisible = false);
        }
    }
}

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:

using System;
using System.Collections.Generic;
using System.Linq;

namespace DataGridRowFiltering
{
    public static class Extensions
    {
        /// <summary>
        /// Applies an action to each item in the sequence, which action depends on the evaluation of the predicate.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <param name="source">A sequence to filter.</param>
        /// <param name="predicate">A function to test each element for a condition.</param>
        /// <param name="posAction">An action used to mutate elements that match the predicate's condition.</param>
        /// <param name="negAction">An action used to mutate elements that do not match the predicate's condition.</param>
        /// <returns>The elements in the sequence that matched the predicate's condition and were transformed by posAction.</returns>
        public static IEnumerable<TSource> ApplyMutateFilter<TSource>(this IEnumerable<TSource> source,
                                                                      Func<TSource, bool> predicate,
                                                                      Action<TSource> posAction,
                                                                      Action<TSource> negAction)
        {
            if (source != null)
            {
                foreach (TSource item in source)
                {
                    if (predicate(item))
                    {
                        posAction(item);
                    }
                    else
                    {
                        negAction(item);
                    }
                }
            }

            return source.Where(predicate);
        }
    }
}

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:

App Running Post-Filter

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.

Posted in DataGrid, Extension Method, Silverlight | Tagged: , , , | 3 Comments »

Silverlight 2.0 Stock List Demo – Part 2

Posted by petermcg on April 24, 2008

Edited on 26/June/2008 : Code download updated to support Silverlight 2 Beta 2

Edited on 20/October/2008 : Code download updated to support Silverlight 2 RTW

Part Two : From XML file to DataGrid via WCF – (Code download)

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 :

Solution Structure

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 :

[DataContract]
public class Stock
{
    [DataMember]
    public string Symbol { get; set; }
    [DataMember]
    public double Bid { get; set; }
    [DataMember]
    public double Ask { get; set; }
}

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 :

<data:DataGrid x:Name="StocksGrid"
    Grid.Row="1"
    Grid.ColumnSpan="2"
    GridlinesVisibility="Horizontal"
    HeadersVisibility="Column"
    RowBackground="AliceBlue"
    IsReadOnly="True"
    CanUserResizeColumns="False"
    Margin="0,5,0,0">
    <data:DataGrid.Columns>
        <data:DataGridTemplateColumn Header="Symbol">
            <data:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Symbol}" />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellTemplate>
        </data:DataGridTemplateColumn>
        <data:DataGridTemplateColumn Header="Bid">
            <data:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Bid}" />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellTemplate>
        </data:DataGridTemplateColumn>
        <data:DataGridTemplateColumn Header="Ask">
            <data:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Ask}" />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellTemplate>
        </data:DataGridTemplateColumn>
    </data:DataGrid.Columns>
</data:DataGrid>


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 :

public partial class Page : UserControl
{
    private readonly StockTicker stockTicker = new StockTicker();

    public Page()
    {
        InitializeComponent();

        StocksGrid.ItemsSource = stockTicker.StockList;

        stockTicker.GetStocksAsync();
    }
}

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:

public class StockTicker
{
    private ObservableCollection<WcfStockService.Stock> stockList = new ObservableCollection<WcfStockService.Stock>();
    public ObservableCollection<WcfStockService.Stock> StockList
    {
        get { return stockList; }
    }

    public void GetStocksAsync()
    {
        BasicHttpBinding binding = new System.ServiceModel.BasicHttpBinding();
        binding.MaxReceivedMessageSize = int.MaxValue;

        EndpointAddress endpoint = Utils.GetHostEndpointAddress("StockService.svc");

        WcfStockService.StockServiceClient proxy = new WcfStockService.StockServiceClient(binding, endpoint);
        proxy.GetStocksCompleted += new EventHandler<WcfStockService.GetStocksCompletedEventArgs>(OnGetStocksCompleted);
        proxy.GetStocksAsync();
    }

    private void OnGetStocksCompleted(object sender, WcfStockService.GetStocksCompletedEventArgs e)
    {
        e.Result.ForEach(stock => StockList.Add(stock));
    }
}

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:

Proxy Collection Type

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 :

Quota Exceeded Exception

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 :

<binding name="BasicHttpBinding_IStockService" maxBufferSize="65536"
    maxReceivedMessageSize="65536">
    <security mode="None" />
</binding>

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 :

Stock List DataGrid

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.

Posted in LINQ to XML, Silverlight, WCF | Tagged: , , , , , | 2 Comments »

Silverlight 2 Stock List Demo – Part 1

Posted by petermcg on April 17, 2008

Edited on 26/June/2008 : Code download updated to support Silverlight 2 Beta 2

Edited on 20/October/2008 : Code download updated to support Silverlight 2 RTW

Part One : From XML file to DataGrid via LINQ to XML – (Code download)

One of the great features new to Silverlight 2.0 Beta 1 is the support for LINQ to XML available in the System.Core and System.Xml.Linq assemblies. Part 1 of this post shows a simple Silverlight application that populates a DataGrid with sample financial stock information read from an XML file using LINQ to XML.

This is the layout of our initial Visual Studio 2008 solution structure :

Solution Structure

Our solution contains three projects, the Silverlight Application project StockListDemo.App and it’s hosting Web Site, and a Silverlight Class Library (StockLibrary). Notice in our Silverlight Application we have a folder called Data containing an XML file pre-populated as follows :

<?xml version="1.0" encoding="utf-8" ?>
<stocks>
  <stock symbol="AAHH" bid="256.02" ask="256.70" />
  <stock symbol="AAPM" bid="181.31" ask="182.03" />
  <stock symbol="AAZG" bid="286.51" ask="286.93" />
  <stock symbol="ABOO" bid="226.74" ask="227.69" />
  <stock symbol="ADJS" bid="109.35" ask="109.92" />
  <stock symbol="AELX" bid="91.52" ask="92.43" />
  ...
</stocks>

Each <stock /> element in the Stocks.xml file consists of a symbol name for the stock, a bid price and an ask price. Although it’s easy to see where the file resides at design time, it’s not immediately obvious how to access it at runtime. When in this situation it’s a good idea to check the ‘Build Action’, you can do this by clicking on the file and pressing F4 :

Stocks.xml Build Action

When you add a new XML file to a Silverlight Application in Visual Studio 2008 the Build Action is set to ‘Content’ by default which means the file and it’s folder structure will be packaged at the root of the Silverlight .xap deployment package. A .xap file is packaged when you build a Silverlight Application by the Chiron.exe SDK tool and is really just a .zip file. If we open it in a .zip file application after a build you can see the Data folder containing the .xml file :

Stocks.xml in Xap Extract

Now we know where the file is at runtime, the question is how do we get at it and read it. Here’s one solution, but the title of this post says LINQ to XML so here’s the code to read the 1000 stock elements from the XML file in the .xap and assign the results to a collection :

public List<Stock> GetStocks()
{
    List<Stock> stocks = new List<Stock>();

    XElement doc = XElement.Load("Data/Stocks.xml");

    stocks = (from el in doc.Elements()
              select GetStock(el)).ToList();

    return stocks;
}

private Stock GetStock(XElement el)
{
    Stock s = new Stock();
    s.Symbol = el.Attribute("symbol").Value;
    s.Bid = Convert.ToDouble(el.Attribute("bid").Value);
    s.Ask = Convert.ToDouble(el.Attribute("ask").Value);
    return s;
}

The two methods above are part of the XmlFileStockService class in the StockLibrary project, you can download the whole small example solution below. The GetStocks method is required by the IStockService interface which the XmlFileStockService class implements. Another class in the same assembly is the StockTicker class. StockTicker has a private field of type IStockService and calls GetStocks on this field to expose the collection in the StockList property. Page.xaml has an instance of StockTicker and assigns it’s StockList property to the ItemsSource property of a DataGrid called StocksGrid with AutoGenerateColumns="True" :

Stock List DataGrid

In a real world situation this data would come from a source such as a web service, now that Silverlight supports cross-domain calls, the data for this type of RIA application can really come from a wide variety of sources. This is a sample application but with the IStockService interface has still been designed to require a minimum of changes for swapping out the datasource from an XML file to a WCF service for example. Some of the areas I intend to take a look at in future posts in this series are : incorporating a web service as a datasource, getting the stock list to tick with modifications and additions and more.

Posted in LINQ to XML, Silverlight | Tagged: , , , , | 2 Comments »

 
Follow

Get every new post delivered to your Inbox.