Friday, May 28, 2010

Handle Checked Items in Silverlight DataGrid Generically


I like the DataGrid in Silverlight, however managing check box colunms is a pain; there is not at easy way to find the items where a particular box is checked. It is not impossible, but it requires working through the visual tree like a mad spider-monkey. It is certainly not as convenient as using a foreach loop, or linq expression to find the items.


An easier way is to bind the checkboxes to boolean values of the items attached to the DataGrid’s ItemsSource. The most coherent way to do this is with a generic class that exposes the checkbox-bound values while providing access to the real data.


In my case, I had to allow the user to mark which items in the DataGrid needed to be deleted, and then delete those items when a ‘Delete All Selected’ button was pressed.

public class SelectedData< T > : INotifyPropertyChanged
{
    public SelectedData(T t)
    {
        Data = t;
    }
    protected bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set { _isSelected = value; Notify("IsSelected"); }
    }

    protected T _data;
    public T Data
    {
        get { return _data; }
        set { _data = value; Notify("Data"); }
    }
  
    protected void Notify( string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

To get this to work with the ItemsSource of the DataGrid, a collection of these wrappers has to be bound to the DataGrid's ItemsSource property. The ObservableCollection works well because the DataGrid will pay attention to the NotifyPropertyChanged event and automatically adjust the items it displays for any changes. If the source collection is also an ObservableCollection, which in my situation it was, we can attach an event to its NotifyPropertyChanged and automatically wrap or delete any additions.


Here is an example:

public class SelectedDataCollection< T > : ObservableCollection< T >
{
    public SelectedDataCollection() { }

    public SelectedDataCollection(ObservableCollection oc)
    {
        SourceCollection = oc;
    }

    private ObservableCollection _sourceCollection;
    public ObservableCollection SourceCollection
    {
        get { return _sourceCollection; }
        set
        {
            UnhookCollectionChanged();
            _sourceCollection = value;
            HookCollectionChanged();
            Repopulate();
        }
    }

    protected void UnhookCollectionChanged()
    {
        if (_sourceCollection != null)
            _sourceCollection.CollectionChanged -= _sourceCollection_CollectionChanged;
    }

    protected void HookCollectionChanged()
    {
        _sourceCollection.CollectionChanged += new
           NotifyCollectionChangedEventHandler(_sourceCollection_CollectionChanged);
    }

    protected void _sourceCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
            foreach (T item in e.NewItems)
                Add(new SelectedData(item));

        if (e.OldItems != null)
        {
            ObservableCollection us = this;
            var toRemove = us.Where(x => e.OldItems.Contains(x.Data)).ToList();
            foreach (var item in toRemove)
            {
                Remove(item);
            }
        }
    }

    protected void Repopulate()
    {
        Clear();
        foreach (T item in _sourceCollection)
        {
            Add(new SelectedData(item));
        }
    }
}

In code-behind, I attach the DataGrid’s ItemSource to an instance of the SelectedDataCollection and then any changes to the source collection automatically ripple through – additions and deletions alike.


Taking it a step further


As you probably know, Silverlight (and WPF) have a notion of a ‘converter’ that can be used in markup to convert or manipulate a particular value before it is set to the control’s property. A converter, using reflection, can find out the type of the source collection and automatically instantiate a SelectedDataCollection of the right type and hook the two together.


Here is the converter's Convert method where all the magic occurs, (error checking for brevity):

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
    var typeOfValue = value.GetType();

    Type paramType = typeOfValue.GetGenericArguments()[0];
    Type collectionType = typeof(SelectedDataCollection<>).MakeGenericType(paramType);
    object newInstance = Activator.CreateInstance(collectionType, value);

    return newInstance;
}

Here is the binding statement in XAML:

ItemsSource="{Binding CctProducts, Converter={StaticResource SE}}"

Where SE is my converter defined in the resources section of my page.


Now I can just bind ItemsSource directly to the source collection without needing to rely on any code-behind to set up the two collections.

No comments: