C#, Generics, XAML

Binding to a Checkbox List in XAML

This technique is useful outside of XAML bindings, too. I found myself working with a person object that can belong to 0 or more groups. The database spec is that it is a many-to-many relationship, something that RIA services chokes on at this time.

Group
- ID
- GroupName
- GroupDescription

Person
- ID
- FirstName
- LastName

PersonXGroup
- PersonID
- GroupID
- CreationTimeStamp // this is here because RIA doesn't play nice with *-* relationships

So, imagine here that there are two foreign keys set up on this model, you know where they go. You need to bind to two different things: The master list which contains all available groups, and the groups that this person has enabled. So, how would you go about binding this list to an ItemsControl filled with checkboxes that allow you to update the state? Use a CheckboxBinder object, which follows.


///

/// Given a master list of items and an object containing a subset of these items,
/// such as a list of groups available and a person belonging to none, some, or all groups,
/// this class allows for binding this list to an itemscontrol with checkboxes to
/// turn on/off items by adding or removing the groups from the person.
/// Adding and removing the items once chnages are to be committed can be handled
/// in a couple ways: (a) By hand, looping through the collection and adding or removing
/// each one, using HasChanged() to see what items have changed, or
/// (b) assign a delegate action for the work to be done when the state changes.
///

///
public class CheckboxBinder
{

public T Item { get; set; }

///

/// Gets or sets the current checked state for this item.
/// If the ItemCheckedChanged delegate is not null, it is called when the state of the item
/// changes.
///

public bool IsChecked {
get { return _isChecked; }
set {
if (_isChecked != value) {
_isChecked = value;
if (ItemCheckedChanged != null) { ItemCheckedChanged(Item, IsChecked); }
}
}
}
private bool _isChecked;

///

/// Optional delegate that is called when IsChecked has changed.
/// Returns the item and its current check state.
///

public Action ItemCheckedChanged { get; set; }

//////////////////////////////////////////////////////

private bool InitiallyChecked { get; set; }

//////////////////////////////////////////////////////

public CheckboxBinder(T item, bool initiallyChecked) {
Item = item;
_isChecked = initiallyChecked;
InitiallyChecked = initiallyChecked;
}

public bool HasChanged() {
return (IsChecked != InitiallyChecked);
}

}

It’s a small class with a lot of power. On your viewmodel (you are using viewmodels, right?) define a collection of ObservableCollection<CheckboxBinder>. This is what you bind your ItemsControl to in your XAML code.

public ObservableCollection<CheckboxBinder> Groups { get; private set; }

When a Person object has been loaded for editing, I populate this list. The constructor for the CheckboxBinder object requires an instance of the item to be bound to, and the initial state as a boolean. The current state of the object is controlled by a bool property called HasChanges(), which compares the initial state to the current state. However, that would be used in a scenario where, when you press the Save button, your code would loop through the entries or use a LINQ statement to grab everything that has changes and then perform an action on it. However, that breaks up the functionality into multiple places throughout your code file, so I enabled another method of updating the collection with the use of an Action property called ItemCheckChanged:

///

/// Handles loading the groups and performing any changes, using the CheckboxBinder's
/// provided change delegate.
///

private void loadGroups() {
Groups = new ObservableCollection<CheckboxBinder>();
foreach (Group group in Reference.PersonData.Groups) {
var cbgroup = new CheckboxBinder(group, CurrentPerson.PersonXGroups.Any(g => g.Group == group));
cbgroup.ItemCheckedChanged = (item, isChecked) => {
if (isChecked) {
CurrentPerson.PersonXGroups.Add(new PersonXGroup() { Group = item, Person = CurrentPerson });
} else {
PersonXGroup pxg = CurrentPerson.PersonXGroups.Single(px => px.Group == item);
//CurrentPerson.PersonXGroups.Remove(pxg);
Reference.PersonData.Delete(pxg);
}
};
Groups.Add(cbgroup);
}
}

You pass ItemCheckedChanged a void delegate with 2 parameters: The item being modified and its current state. It’s only called when the value actually changes (so if the current state is false and you change the value to false, this will not be called). The update code handles adding and removing items from the collection.

if (isChecked) {
CurrentPerson.PersonXGroups.Add(new PersonXGroup() { Group = item, Person = CurrentPerson });
} else {
PersonXGroup pxg = CurrentPerson.PersonXGroups.Single(px => px.Group == item);
//CurrentPerson.PersonXGroups.Remove(pxg);
Reference.PersonData.Delete(pxg);
}

This way, you’re not iterating through a loop at the start of your Save routine. However, this is possible because I can just RejectChanges on my data context if I so desire – but the other aforementioned method works well if you don’t have the ability to roll back changes like you can on a data context.

Here’s the XAML used to bind to the checkbox list:
<sdk:Label Content="Groups" Style="{StaticResource FormHeaderStyle}" Margin="5,10,5,0" />
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Path=Groups}" Height="130">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Margin="2" Content="{Binding Path=Item.GroupName}" ToolTipService.ToolTip="{Binding Path=Item.GroupDescription}" IsChecked="{Binding Path=IsChecked, Mode=TwoWay}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>

Has this helped you? Do you know of a better way to accomplish the same task? Shout it out in the comments.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s