Thursday, February 10, 2011

MVVM - Observable INotifyPropertyChanged.PropertyChanged Event Stream Using Reactive Extension

In this post we are going to discuss how we can use observable event streams using Reactive Extension. Since we have been trying to apply this knowledge to MVVM and seeking what benefit we can obtain from Rx, we will be mostly interested in those events which we deal with the most frequently. One such event is PropertyChanged event of INotifyPropertyChanged.

Let’s start with a sample WPF application. As is general in this blog, We will start with a very simple example. Then we will keep adding complexity to it and discuss why we actually need them. Below is a typical WPF window. It just has a button. We need to show another window to enter the details about student.

<Window x:Class="WpfApp_MVVM_ReactiveModelSynch.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Content="Show Student Form" Height="47" HorizontalAlignment="Left"
Margin="104,244,0,0" Name="button1" VerticalAlignment="Top" Width="318" Click="button1_Click" />
</Grid>
</Window>

Since these details need to be shared between different StudentView windows, we are defining the Student object as instance variable. When user clicks the window, we are constructing the view model on the fly but we keep passing the same Student instance. This would enable the information to be shared between different windows. We are calling Show() on this new instance of StudentView. As you know this would show this window as a Modeless window. This is to demonstrate the scenarios in which we might not instantiate view models inside the view but we need to hand it over, somehow, to the view.

public partial class MainWindow : Window
{
private Student _student;

public MainWindow()
{
InitializeComponent();

_student = new Student();
}

private void button1_Click(object sender, RoutedEventArgs e)
{
new StudentView(new StudentViewModel(_student)).Show();
}
}



Now we have a look at the definition of StudentView. It just has two text boxes for user entry. The information regarding Id and Name of Student can be entered in those text boxes. These text boxes are bound to StudentId and StudentName properties from the DataContext.

<Window x:Class="WpfApp_MVVM_ReactiveModelSynch.StudentView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="StudentView" Height="300" Width="484">
<Grid>
<Label Content="Id" Height="26" HorizontalAlignment="Left" Margin="11,30,0,0"
Name="labelId" VerticalAlignment="Top" Width="73" />
<TextBox Height="26" HorizontalAlignment="Left" Margin="90,30,0,0"
Name="textBoxId" VerticalAlignment="Top" Width="362"
Text="{Binding Path=StudentId, UpdateSourceTrigger=PropertyChanged}"/>
<Label Content="Name" Height="25" HorizontalAlignment="Left" Margin="11,76,0,0"
Name="labelName" VerticalAlignment="Top" Width="73" />
<TextBox Height="26" HorizontalAlignment="Left" Margin="90,75,0,0" Name="textBoxName"
VerticalAlignment="Top" Width="362"
Text="{Binding Path=StudentName, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>

In the code behind, we assign the view model passed as constructor argument to the DataContext property of the view.

public partial class StudentView : Window
{
public StudentView(StudentViewModel studentViewModel)
{
InitializeComponent();

this.DataContext = studentViewModel;
}
}



As is expected by the view, StudentViewModel has two properties StudentId and StudentName. Both support change notification using INotifyPropertyChanged’s PropertyChanged event. As is specified in the MainWindow.xaml.cs, we are using Student object as constructor parameter. We are assigning it to the instance variable _model. We are using _model as Model for this view. WPF Binding system would update the values of these properties as the user enters data in the text boxes bound to these properties. In the setters of these properties, we are just assigning these values to the corresponding properties in _model. This should keep model being updated. Since this is shared by the view models used by each StudentView object, we expect the other views to be updated as these changes are applied.

public class StudentViewModel : INotifyPropertyChanged
{
private int _studentId;
private string _studentName;

private Student _model;

public StudentViewModel(Student student)
{
_model = student;
}

public int StudentId
{
get { return _studentId; }
set
{
if (_studentId != value)
{
_studentId = value;
_model.StudentId = value;
OnPropertyChanged("StudentId");
}
}
}

public string StudentName
{
get { return _studentName; }
set
{
if(_studentName != value)
{
_studentName = value;
_model.StudentName = value;
OnPropertyChanged("StudentName");
}
}
}

public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

As you might have guessed from the above code, Student has two properties StudentId and StudentName. This also implements INotifyPropertyChanged so needs to define PropertyChanged event. Whenever the values of these properties are changed, PropertyChanged event is raised to notify all observers about this change. Though it does not provide the actual change but it does tell about the property updated. We can handle this event in the code using StudentModel and act accordingly.

public class Student : INotifyPropertyChanged
{
private int _studentId;
private string _studentName;

public int StudentId
{
get { return _studentId; }
set
{
if (_studentId != value)
{
_studentId = value;
OnPropertyChanged("StudentId");
}
}
}

public string StudentName
{
get { return _studentName; }
set
{
if (_studentName != value)
{
_studentName = value;
OnPropertyChanged("StudentName");
}
}
}

public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

Now let’s run this! We open two StudentView windows. We enter data in one window. The data is not being updated in the second window. Why is this?? As is apparent in the code of view model, as we receive updates in View Model’s properties through WPF Binding System, we are passing the same updates to the corresponding properties in the model.


If you put break points in the model properties’ setter, you would notice that the setter code is being executed. Even the PropertyChanged event is being raised. We just need to handle this event in the view model so that the view model could update its properties. Since these properties are supporting change notifications using INotifyPropertyChanged, the bound element in the view should also be updated accordingly. As discussed, let’s subscribe PropertyChanged event in the view model for model object.
_model.PropertyChanged += new PropertyChangedEventHandler(_model_PropertyChanged);

The definition of _model_PropertyChanged is as follows:

void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
object value = _model.GetType().GetProperty(e.PropertyName).GetValue(_model, null);
this.GetType().GetProperty(e.PropertyName).SetValue(this, value, null);
}

This is based on reflection and based on the assumption that View Model has properties with same names. Their types are also assumed to be supportive. Since we only have limited properties, we can also avoid reflection and update each property. Let’s do that.

void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.StudentId = _model.StudentId;
this.StudentName = _model.StudentName;
}

Now let’s run this application. We will again open two windows and enter data in one window and observe if the same updates are made in the second window.


As expected, the updates are applied in the second window.

Now since we have fulfilled whatever the requirements have stated, we should be the happiest person in the world. But we can certainly improve on this implementation. We needed to handle model’s PropertyChanged event in StudentViewModel. We can avoid these by using observable event stream support available in Reactive Extension (Rx). Not only it allows to subscribe to the observable event stream, it also allows a filtered subscription. So We will be subscribing to a subset of events using Where clause even if there is an active event stream. It is also possible that we are receiving frequent updates from the model, like millions in a second. We don’t want to update our view so frequently. Similarly, since we have set UpdateSourceTrigger as PropertyChanged for both binding in the view. All the changes in the Text property of the text box will cause the property setter to execute. It is possible that the user of this system types very frequently, we might just want to update the model when it settles and not after each key entry. In short we might need throttling support for these property changes.

Using Observable Event Stream for model’s PropertyChanged event:
Before adding the code described in this section remove model’s PropertyChanged handling logic from StudentViewModel. This includes event subscription in the constructor and definition of event handler. Before making the updates suggested above, we need to reference assemblies provided by Rx installer. We need to add references for following assemblies:

1. System.Reactive.dll
2. System.CoreEx


These assemblies are installed as part of installation of Reactive Extension (Rx). It can be downloaded as follows:

http://msdn.microsoft.com/en-us/devlabs/ee794896

Update the code of the constructor of StudentViewModel as follows:

public StudentViewModel(Student student)
{
_model = student;

Observable.FromEvent<PropertyChangedEventArgs>(_model, "PropertyChanged")
.Subscribe((arg) =>
{
this.StudentId = _model.StudentId;
this.StudentName = _model.StudentName;
});
}

System.Linq.Observable from System.Reactive assembly allows to subscribe with any event stream. System.Collections.Generic.IEvent<T> is defined in System.CoreEx assembly. We are subscribing to PropertyChanged event stream from _model instance. Observable.FromEvent<T>() returns IObservable<IEvent<T>>. Since it is an IObservable, we can subscribe to it. The code that we want to execute when event is triggered can be specified in the onNext delegate. Let's run the application now.



Like before, we are opening two instances of StudentView window. When we enter information in the text boxes, information is updated in other window. The contents are updated because of the event handling logic in the observable event stream handler code as presented above.

Filtering events from Event Stream:
Since it is an IObservable<T> we can filter it using Where operator. Let's filter it to just handle the case when event is received because of update in StudentName property. The changes in StudentId are ignored. Let's update the code as follows:

public StudentViewModel(Student student)
{
_model = student;

Observable.FromEvent<PropertyChangedEventArgs>(_model, "PropertyChanged")
.Where(et => et.EventArgs.PropertyName == "StudentName")
.Subscribe((arg) =>
{
this.StudentId = _model.StudentId;
this.StudentName = _model.StudentName;
});
}

Now run the application and again open two instances. The update in text box for StudentName shows the updates in the other window. If we update StudentId, it doesn't update the other window as the event stream is filtered.



Throttling events from Event Stream:
Since are binding using UpdateSourceTrigger as PropertyChanged, as the user types in the text boxes in StudentView, WPF Binding System automatically updates the values in the View model's properties. This updates corresponding properties of the model using the view model's property setters. We just don't want to keep sending updates to the model but we should wait until the user is done typing and then update the model's properties. This can be supported by throttling support. This can by achieved introducing PropertyChanged event in the view models for itself. We can remove the code to update corresponding model's properties in the setters. Let's update the definition of StudentViewModel as follows:

public class StudentViewModel : INotifyPropertyChanged
{
private int _studentId;
private string _studentName;

private Student _model;

public StudentViewModel(Student student)
{
_model = student;

Observable.FromEvent<PropertyChangedEventArgs>(_model, "PropertyChanged")
.Where(et => et.EventArgs.PropertyName == "StudentName")
.Subscribe((arg) =>
{
this.StudentId = _model.StudentId;
this.StudentName = _model.StudentName;
});



Observable.FromEvent<PropertyChangedEventArgs>(this, "PropertyChanged")
.Throttle(TimeSpan.FromMilliseconds(500))
.Where(et => et.EventArgs.PropertyName == "StudentName")
.Subscribe((ar) =>
{
_model.StudentId = this.StudentId;
_model.StudentName = this.StudentName;
});

}

//void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
//{
// //object value = _model.GetType().GetProperty(e.PropertyName).GetValue(_model, null);
// //this.GetType().GetProperty(e.PropertyName).SetValue(this, value, null);
// //this.StudentId = _model.StudentId;
// //this.StudentName = _model.StudentName;
//}

public int StudentId
{
get { return _studentId; }
set
{
if (_studentId != value)
{
_studentId = value;
//_model.StudentId = value;
OnPropertyChanged("StudentId");
}
}
}

public string StudentName
{
get { return _studentName; }
set
{
if(_studentName != value)
{
_studentName = value;
//_model.StudentName = value;
OnPropertyChanged("StudentName");
}
}
}

public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

We needed to remove the code resulting in the updates in the setter of properties in the view model. Let' throttle these updates. When user types in StudentName, the IObservable doesn't update the other window immediately. It is rather waiting for property to settle down. After which it is causing the PropertyChanged events to be handled.



Download:

No comments: