Thursday, October 28, 2010

WPF - Style Selector for Items in ItemsControl

Using ItemsControl, we can display list of data in WPF view. It is, sometimes, a requirement that data with different values appear differently on the view so that user focus could be triggered on important information in the list. WPF supports this treatment of items in ItemsControl through Selector.

Selector is available in System.Windows.Controls namespace. In order to define custom selectors, we need to inherit from this and override SelectStyle method. This method provides you the item as an argument. We can look at different properties of the item and apply specific style to it.

Lets have a look at an example where we could use StyleSelector. In this example we are creating a Window with a ComboBox and ListBox. We have to show the same list of students in both controls. Those students which are not Enrolled in a particular program yet should be highlighted with red background. This would help the user to identify students and helping them with enrollment (if required).

The definition of View is as follows. This view is using WindowStyleSelectorDemonstratorViewModel as its DataContext. It is expecting that the DataContext has some collection which is bound to its ComboBox and ListBox. It is also defining StudentItemStyleSelector as a resource and using it as ItemContainerStyleSelector for ComboBox and ListBox.

<Window x:Class="WPFCommandLineArguments.WindowStyleSelectorDemonstrator"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFCommandLineArguments"
Title="WindowStyleSelector" Height="300" Width="566">
<Window.Resources>
<local:StudentItemStyleSelector x:Key="studentStyle" />
</Window.Resources>
<Window.DataContext>
<local:WindowStyleSelectorDemonstratorViewModel />
</Window.DataContext>
<Grid>
<Label Content="Students" Height="22" Margin="20,18,350,0"
Name="label1" VerticalAlignment="Top" Target="{Binding ElementName=comboStudents}" />
<ComboBox Height="21" Margin="20,46,0,0" Name="comboStudents"
VerticalAlignment="Top" HorizontalAlignment="Left" Width="174"
ItemsSource="{Binding Path=Students, Mode=OneWay}" DisplayMemberPath="StudentName"

SelectedValuePath="StudentId"
ItemContainerStyleSelector="{DynamicResource studentStyle}" />
<Label Content="Students" Height="22" Margin="247,18,123,0" Name="label2"
VerticalAlignment="Top" Target="{Binding ElementName=listBoxStudents}" />
<ListBox Height="195" HorizontalAlignment="Left" Margin="247,46,0,0"
Name="listBoxStudents" VerticalAlignment="Top" Width="246" ItemsSource="{Binding Students}"
ItemContainerStyleSelector="{DynamicResource studentStyle}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding StudentName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>

The code behind for WindowStyleSelectorDemonstrator is as follows:

public partial class WindowStyleSelectorDemonstrator : Window
{
public WindowStyleSelectorDemonstrator()
{
InitializeComponent();
}
}

As expected by the view, we create the view model WindowStyleSelectorDemonstratorViewModel. We define an ObservableCollection named Students which is used as ItemsSource by the ComboBox and ListBox in the View.

class WindowStyleSelectorDemonstratorViewModel : DependencyObject
{
public ObservableCollection Students
{
get { return (ObservableCollection) GetValue(StudentsProperty); }
private set { SetValue(StudentsProperty, value);}
}

public WindowStyleSelectorDemonstratorViewModel()
{
Students = new ObservableCollection();
Students.Add(new Student{StudentId = 1, StudentName = "Muhammad", IsEnrolled = true});
Students.Add(new Student { StudentId = 1, StudentName = "Jim", IsEnrolled = false });
Students.Add(new Student { StudentId = 1, StudentName = "Mark", IsEnrolled = false });
Students.Add(new Student { StudentId = 1, StudentName = "Peter", IsEnrolled = true });
Students.Add(new Student { StudentId = 1, StudentName = "Daisy", IsEnrolled = false });
Students.Add(new Student { StudentId = 1, StudentName = "Jennifer", IsEnrolled = true });
Students.Add(new Student { StudentId = 1, StudentName = "Erwin", IsEnrolled = true });
Students.Add(new Student { StudentId = 1, StudentName = "Mike", IsEnrolled = false });
Students.Add(new Student { StudentId = 1, StudentName = "Jason", IsEnrolled = true });
Students.Add(new Student { StudentId = 1, StudentName = "Asif", IsEnrolled = true });

}

public DependencyProperty StudentsProperty = DependencyProperty.Register("Students",
typeof (ObservableCollection),


typeof(WindowStyleSelectorDemonstratorViewModel));
}

Each item of the collection is of type Student. The expected properties are StudentName, StudentId and IsEnrolled. Lets define this as DependencyObject. We are registering each property as DependencyProperty so that WPF runtime could provide change notification for these properties.

class Student : DependencyObject
{
//Student Id
public static DependencyProperty StudentIdProperty =
DependencyProperty.Register("StudentId", typeof (int), typeof (Student));
public int StudentId
{
get { return (int) GetValue(StudentIdProperty); }
set { SetValue(StudentIdProperty, value);}
}

//Student Name
public static DependencyProperty StudentNameProperty =
DependencyProperty.Register("StudentName", typeof (string), typeof (Student));
public string StudentName
{
get { return (string) GetValue(StudentNameProperty); }
set {SetValue(StudentNameProperty, value);}
}

//IsEnrolled
public static DependencyProperty IsEnrolledProperty =
DependencyProperty.Register("IsEnrolled", typeof (bool), typeof (Student));
public bool IsEnrolled
{
get { return (bool) GetValue(IsEnrolledProperty); }
set {SetValue(IsEnrolledProperty, value);}
}
}

The most important part of the example is as below. Here we are defining a Selector for Student. Since it is based on ContentControl, we can use it for both ListBoxItem and ComboBoxItem. Had we needed setting any specialized property for ListBoxItem or ComboBoxItem then we would have defined different Selectors for each. In the Selector code below, we are looking at IsEnrolled property of Student. If IsEnrolled is false, we are setting the background color ContentControl to Red. We have used Brushes, from System.Windows.Media, to set the Background brush. It has common brushes defined as static properties.

class StudentItemStyleSelector : System.Windows.Controls.StyleSelector
{
public override Style SelectStyle(object item, DependencyObject container)
{
Style comboBoxItemStyle = new Style(typeof(Control));

Student student = (Student) item;

if (!student.IsEnrolled)
{
comboBoxItemStyle.Setters.Add(new Setter(Control.BackgroundProperty, Brushes.Red));
}
else
{
comboBoxItemStyle.Setters.Add(new Setter(Control.BackgroundProperty, Brushes.White));
}

return comboBoxItemStyle;
}
}


When we run the application, it should be displayed as follows (after opening the popup of combo box).

No comments: