1. Problem
You have data-bound elements
in your UI, and you want to enable change notifications and automatic
refresh of the UI when the bound application data changes.
2. Solution
You implement the System.ComponentModel.INotifyPropertyChanged interface in your data types and the System.Collections.Specialized.INotifyCollectionChanged
interface in your collection types. You then raise the events defined
in these interfaces from the implementing types to provide change
notifications. You also ensure that the Mode property for each data binding is set to either BindingMode.OneWay or BindingMode.TwoWay to enable automatic UI refresh.
3. How It Works
The Silverlight binding
infrastructure is aware of these two special interfaces and
automatically subscribes to change notification events defined in the
interfaces when implemented by the data source types.
3.1. Change Notification for Noncollection Types
The INotifyPropertyChanged interface has a single event named PropertyChanged. The event parameter is of type PropertyChangedEventArgs, which accepts the name of the changing property as a string parameter to the constructor and exposes it through the PropertyName property. The PropertyChangedEvntArgs class is shown here:
public class PropertyChangedEventArgs : EventArgs
{
// Fields
private readonly string propertyName;
// Methods
public PropertyChangedEventArgs(string propertyName);
// Properties
public string PropertyName { get; }
}
Once you implement the INotifyPropertyChanged interface in your data source type, you raise PropertyChanged
whenever you need to raise change notifications for any of the bound
source properties. You pass in the name of the property being changed
through an instance of PropertyChangedEventArgs. Listing 1 shows a small but standard sample implementation.
Listing 1. Sample Implementation of INotifyPropertyChanged
public class Notifier : INotifyPropertyChanged
{
//implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
//utility method to raise PropertyChanged
private void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
private string _SomeBoundProperty;
public string SomeBoundProperty
{
get { return _SomeBoundProperty; }
set
{
//save old value
string OldVal = _SomeBoundProperty;
//compare with new value
if (OldVal != value)
{
//if different, set property
_SomeBoundProperty = value;
//and raise PropertyChanged
RaisePropertyChanged(new
PropertyChangedEventArgs("SomeBoundProperty"));
}
}
}
}
|
3.2. Change Notification for Collection Types
The INotifyCollectionChanged interface also has a single event, named CollectionChanged,
which can be raised by implementing collection types to provide change
notifications. The change information that can be gained for collections
is richer in comparison to INotifyPropertyChanged, as you can see in the NotifyCollectionChangedEventArgs type listed here:
public sealed class NotifyCollectionChangedEventArgs : EventArgs
{
// Other members omitted for brevity
public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action,
object newItem, object oldItem, int index);
public NotifyCollectionChangedAction Action { get; }
public IList NewItems { get; }
public int NewStartingIndex { get; }
public IList OldItems { get; }
public int OldStartingIndex { get; }
}
The code sample in the next section shows a custom collection that implements INotifyCollectionChanged.
4. The Code
The sample code for this recipe builds a simple data entry form over the data struc tures in Listing 2.
Listing 2. Application Data Classes
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
namespace Recipe4_3
{
public class Employee : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
public Employee()
{
}
private string _FirstName;
public string FirstName
{
get { return _FirstName; }
set
{
string OldVal = _FirstName;
if (OldVal != value)
{
_FirstName = value;
RaisePropertyChanged(new PropertyChangedEventArgs("FirstName"));
}
}
}
private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
string OldVal = _LastName;
if (OldVal != value)
{
_LastName = value;
RaisePropertyChanged(new PropertyChangedEventArgs("LastName"));
}
}
}
private long _PhoneNum;
public long PhoneNum
{
get { return _PhoneNum; }
set
{
long OldVal = _PhoneNum;
if (OldVal != value)
{
_PhoneNum = value;
RaisePropertyChanged(new PropertyChangedEventArgs("PhoneNum"));
}
}
}
private Address _Address;
public Address Address
{
get { return _Address; }
set
{
Address OldVal = _Address;
if (OldVal != value)
{
_Address = value;
RaisePropertyChanged(new PropertyChangedEventArgs("Address"));
}
}
}
}
public class Address : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
private string _Street;
public string Street
{
get { return _Street; }
set
{
string OldVal = _Street;
if (OldVal != value)
{
_Street = value;
RaisePropertyChanged(new PropertyChangedEventArgs("Street"));
}
}
}
private string _City;
public string City
{
get { return _City; }
set
{
string OldVal = _City;
if (OldVal != value)
{
_City = value;
RaisePropertyChanged(new PropertyChangedEventArgs("City"));
}
}
}
private string _State;
public string State
{
get { return _State; }
set
{
string OldVal = _State;
if (OldVal != value)
{
_State = value;
RaisePropertyChanged(new PropertyChangedEventArgs("State"));
}
}
}
private int _ZipCode;
public int ZipCode
{
get { return _ZipCode; }
set
{
int OldVal = _ZipCode;
if (OldVal != value)
{
_ZipCode = value;
RaisePropertyChanged(new PropertyChangedEventArgs("ZipCode"));
}
}
}
}
public class EmployeeCollection : ICollection<Employee>,
IList<Employee>,
INotifyCollectionChanged
{
private List<Employee> _internalList;
public EmployeeCollection()
{
_internalList = new List<Employee>();
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
private void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
{
CollectionChanged(this, e);
}
}
//Methods/Properties that would possibly change the collection and its content
//need to raise the CollectionChanged event
public void Add(Employee item)
{
_internalList.Add(item);
RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,
item, _internalList.Count − 1));
}
public void Clear()
{
_internalList.Clear();
RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public bool Remove(Employee item)
{
int idx = _internalList.IndexOf(item);
bool RetVal = _internalList.Remove(item);
if (RetVal)
RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, item, idx));
return RetVal;
}
public void RemoveAt(int index)
{
Employee item = null;
if (index < _internalList.Count)
item = _internalList[index];
_internalList.RemoveAt(index);
if (index < _internalList.Count)
RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, item, index));
}
public void Insert(int index, Employee item)
{
_internalList.Insert(index, item);
RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item, index));
}
public Employee this[int index]
{
get { return _internalList[index]; }
set
{
_internalList[index] = value;
RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace, value, index));
}
}
public bool Contains(Employee item)
{
return _internalList.Contains(item);
}
public void CopyTo(Employee[] array, int arrayIndex)
{
_internalList.CopyTo(array, arrayIndex);
}
public int Count
{
get { return _internalList.Count; }
}
public bool IsReadOnly
{
get { return ((IList<Employee>)_internalList).IsReadOnly; }
}
public IEnumerator<Employee> GetEnumerator()
{
return _internalList.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return (System.Collections.IEnumerator)_internalList.GetEnumerator();
}
public int IndexOf(Employee item)
{
return _internalList.IndexOf(item);
}
}
}
|
As shown in Listing 2, both the Employee and the Address types implement INotifyPropertyChanged to provide change notification. You also define a custom collection named EmployeeCollection for Employee instances and implement INotifyCollectionChanged on the collection type.
You can see the additional change information that can be accessed through the NotifyCollectionChangedEventArgs. Using the NotifyCollectionChangedAction enumeration, you can specify the type of change (Add, Remove, Replace, or Reset).
You can also specify the item that changed and its index in the
collection. This detail allows the binding infrastructure to optimize
the binding so that the entire UI bound to the collection need not be
refreshed for each change in the collection.
Also note that the System.Collections.ObjectModel contains a generic type named ObservableCollection<T> that already implements INotifyCollectionChanged. For all data binding scenarios, unless you have a specific reason to implement your own collection type, ObservableCollection<T> should meet your needs.
However, ObservableCollection<T> simply extends Collection<T>,
which is a base collection class in the framework. If you choose to
have change notification enabled for some of the other, more advanced,
collections in the framework, such as List<T> or LinkedList<T>, or if you have implemented your own custom collection types with custom business logic, implementing INotifyCollectionChanged is the way to go.
Another scenario where you
might choose to implement a custom collection is if you want to declare
the collection as a resource in your XAML. This would necessitate
creating a nongeneric collection class with a default constructor, and
you would possibly want to initialize such a collection in the
constructor. You can, however, extend ObservableCollection
directly in such cases and do away with the need to implement any of
the collection manipulation methods shown in the previous sample.
Listing 3 shows the simple data entry UI that you build on top of this collection.
Listing 3. Data Entry UI XAML
<UserControl x:Class="Recipe4_3.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Recipe4_3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Width="400"
Height="441">
<UserControl.Resources>
<!-- Employee collection Data source -->
<local:EmployeeCollection x:Key="REF_EmployeeCollection" />
<!-- Data template to be used for the Employee type -->
<DataTemplate x:Key="dtEmployee">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text="{Binding LastName}"
Margin="5,0,0,0" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot"
Background="White"
Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
x:Name="lbx_Employees"
ItemsSource="{StaticResource REF_EmployeeCollection}"
ItemTemplate="{StaticResource dtEmployee}"
SelectionChanged="lbx_Employees_SelectionChanged" />
<Grid x:Name="grid_NewButton"
Margin="0,2,0,0"
Grid.Row="1"
HorizontalAlignment="Right">
<Button x:Name="btn_New"
Click="btn_New_Click"
Content="New Employee" />
</Grid>
<Border Grid.Row="2"
Visibility="Collapsed"
x:Name="border_EmployeeForm"
Margin="0,2,0,0"
BorderBrush="Black"
BorderThickness="1"
Padding="1,1,1,1">
<Grid x:Name="grid_EmployeeForm">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.142*" />
<ColumnDefinition Width="0.379*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.097*" />
<ColumnDefinition Width="0.082*" />
<ColumnDefinition Width="0.2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.10*" />
<RowDefinition Height="0.15*" />
<RowDefinition Height="0.15*" />
<RowDefinition Height="0.15*" />
<RowDefinition Height="0.45*" />
</Grid.RowDefinitions>
<TextBox HorizontalAlignment="Stretch"
Margin="1,1,1,1"
x:Name="tbxFName"
VerticalAlignment="Stretch"
Text="{Binding FirstName, Mode=TwoWay}"
Grid.Row="1"
Width="Auto"
Grid.RowSpan="1"
Grid.ColumnSpan="2"
Grid.Column="1" />
<TextBox HorizontalAlignment="Stretch"
Margin="1,1,1,1"
x:Name="tbxLName"
VerticalAlignment="Stretch"
Text="{Binding LastName, Mode=TwoWay}"
Grid.Row="1"
Grid.Column="3"
Width="Auto"
Grid.RowSpan="1"
Grid.ColumnSpan="3" />
<TextBlock HorizontalAlignment="Stretch"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="Last"
TextWrapping="Wrap"
Grid.RowSpan="1"
Grid.Column="4"
Grid.ColumnSpan="2"
Height="Auto"
Width="Auto" />
<TextBlock HorizontalAlignment="Center"
Margin="1,1,1,1"
VerticalAlignment="Center"
Text="First"
TextWrapping="Wrap"
Grid.RowSpan="1"
Grid.Column="1"
Width="Auto"
Height="Auto" />
<TextBlock HorizontalAlignment="Center"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="Name"
TextWrapping="Wrap"
Grid.RowSpan="1"
Grid.Row="1"
Height="Auto"
Width="Auto" />
<TextBlock HorizontalAlignment="Center"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="Street"
TextWrapping="Wrap"
Grid.Row="2"
Width="Auto" />
<TextBox HorizontalAlignment="Stretch"
x:Name="tbxStreet"
VerticalAlignment="Stretch"
Text="{Binding Address.Street, Mode=TwoWay}"
Grid.Row="2"
Margin="1,1,1,1"
Grid.Column="1"
Grid.ColumnSpan="5"
Width="Auto" />
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Text="City"
TextWrapping="Wrap"
Margin="1,1,1,1"
Grid.Row="3" />
<TextBlock Text="State"
Margin="1,1,1,1"
TextWrapping="Wrap"
Grid.Column="2"
Grid.Row="3"
HorizontalAlignment="Center" />
<TextBlock Text="Zip"
Margin="1,1,1,1"
TextWrapping="Wrap"
Grid.Column="4"
Grid.Row="3"
HorizontalAlignment="Center" />
<TextBox HorizontalAlignment="Stretch"
x:Name="tbxCity"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="{Binding Address.City, Mode=TwoWay}"
Grid.Row="3"
Grid.Column="1" />
<TextBox Background="Transparent"
Grid.Column="3"
Margin="1,1,1,1"
Grid.Row="3"
Text="{Binding Address.State, Mode=TwoWay }"
x:Name="tbxState">
</TextBox>
<TextBox Background="Transparent"
Grid.Column="5"
Grid.Row="3"
Margin="1,1,1,1"
Text="{Binding Address.ZipCode, Mode=TwoWay }"
x:Name="tbxZipCode" />
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Text="Phone"
Margin="1,1,1,1"
TextWrapping="Wrap"
Grid.Row="4" />
<TextBox Grid.Column="1"
Grid.Row="4"
Margin="1,1,1,1"
Text="{Binding PhoneNum, Mode=TwoWay }"
x:Name="tbxPhoneNum" />
<Button Grid.Column="5"
Margin="1,1,1,1"
Grid.Row="4"
Height="30.911"
VerticalAlignment="Top"
Content="Close"
x:Name="btnClose"
Click="btnClose_Click" />
</Grid>
</Border>
</Grid>
</UserControl>
|
You can see that, for the editable controls, you set the Mode property of the binding to BindingMode.TwoWay. The Mode property can be set to one of three values:
BindingMode.OneTime
binds the value coming from the data source only once, when the element
is initially displayed, and never again during the lifetime of the
application. This is useful for static data that does not change for the
lifetime of the application.
BindingMode.OneWay
refreshes the bound value with any changes that happens to the data
source but does not propagate changes made in the UI to the bound data
source. This is useful for data that is read only to the user but that
can change through other means in the application. This is the default
setting for Binding.Mode if you do not specify any setting in your XAML or code.
BindingMode.TwoWay enables bidirectional propagation of changes and is the suitable mode for data-editing scenarios.
Running the sample produces the output shown in Figure 1.
Listing 4 shows the codebehind for the MainPage. As shown in the constructor, you initialize the bound EmployeeCollection instance with some initial Employee data. If you selected one of the records, you would see the output in Figure 2.
Listing 4. Codebehind for the Page
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
namespace Recipe4_3
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
//initialize the employee collection with some sample data
EmployeeCollection empColl = (EmployeeCollection)lbx_Employees.ItemsSource;
empColl.Add(new Employee
{
FirstName = "Joe",
LastName = "Duffin",
PhoneNum = 2125551212,
Address = new Address
{
Street = "2000 Mott Street",
City = "New York",
State = "NY",
ZipCode = 10006
}
});
empColl.Add(new Employee
{
FirstName = "Alex",
LastName = "Bleeker",
PhoneNum = 7185551212,
Address = new Address
{
Street = "11000 Clover Street",
City = "New York",
State = "NY",
ZipCode = 10007
}
});
empColl.Add(new Employee
{
FirstName = "Nelly",
LastName = "Myers",
PhoneNum = 7325551212,
Address = new Address
{
Street = "12000 Fay Road",
City = "New York",
State = "NY",
ZipCode = 10016
}
});
}
private void btn_New_Click(object sender, RoutedEventArgs e)
{
//get the bound collection
EmployeeCollection empColl = (EmployeeCollection)lbx_Employees.ItemsSource;
//create and initialize a new Employee
Employee newEmp = new Employee();
newEmp.Address = new Address();
//add it to the collection
empColl.Add(newEmp);
//set the current selection to the newly added employee.
//This will cause selection change to fire, and set
//the datacontext for the form appropriately
lbx_Employees.SelectedItem = newEmp;
}
private void lbx_Employees_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
//set the datacontext of the form to the selected Employee
grid_EmployeeForm.DataContext = (Employee)lbx_Employees.SelectedItem;
//show the form
border_EmployeeForm.Visibility = Visibility.Visible;
grid_NewButton.Visibility = Visibility.Collapsed;
}
private void btnClose_Click(object sender, RoutedEventArgs e)
{
//hide the form
border_EmployeeForm.Visibility = Visibility.Collapsed;
grid_NewButton.Visibility = Visibility.Visible;
}
}
}
|
In the SelectionChanged handler for lbxEmployees, named lbx_Employees_SelectionChanged() in Listing 4, you set the DataContext of the containing Grid named grid_EmployeeForm to the selected Employee data item. This populates the contained fields with various properties of the Employee instance based on the bindings defined in Listing 3. You then make the Grid visible.
If you try editing the First Name field, you should see it changing in the selected item in the ListBox once you tab out of the field after the edit. As the data entry form propagates the change back to the appropriate Employee item in the collection as a result of the TwoWay binding, this action, in turn, causes the ListBox's binding to the collection to refresh the selected item.
If you click the New Employee button, you should get a blank data entry form, as shown in Figure 3, and see a blank item added to the ListBox. To achieve this, you handle the Click event of the button in btn_New_Click() shown in Listing 4. You create a new instance of the Employee type, initialize it, and add it to the collection. This takes care of displaying the blank item in the ListBox through the change notification mechanism of INotifyCollectionChanged. You also programmatically make that item the selected item in the ListBox, which in turns fires the SelectionChanged handler of the ListBox, and the data entry form is displayed again, as described in the previous paragraph.
Filling the fields in the data entry form should again cause change notifications to be propagated to the ListBox, as you tab out of fields.