Thursday, February 10, 2011

MVVM - View Model (IObserver) Observing multiple IObservable (s)

This is a continuation of posts of our discussion about the usage of Reactive Extension (Rx) in MVVM implementation in WPF. For this post, we will be trying to find out how a View model observes multiple observables. The main question is, Can an IObserver observes multiple IObservable?

In this example, view model is observing the data from different sources. We want to show the list of students and courses on the same window. I am not saying this is the best thing or I would do it in a real world application. But this is more of a what if kind of a scenario. We want our view to look like this:



Let us update the view in the previous post as follows:

<Window x:Class="WpfApp_MVVM_ReactiveModel.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp_MVVM_ReactiveModel"
Title="MainWindow" Height="544" Width="526">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Label Content="Students" Height="26" HorizontalAlignment="Left" Name="label1"
VerticalAlignment="Top" Width="161" FontWeight="Bold" />
<ListBox Height="234" HorizontalAlignment="Left" Margin="4,30,0,0"
Name="listBox1" VerticalAlignment="Top" Width="493"
ItemsSource="{Binding StudentList}">
<ListBox.ItemTemplate>
<DataTemplate >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding StudentId, StringFormat=Id:{0}}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding StudentName, StringFormat=Name:{0}}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Label Content="Courses" FontWeight="Bold" Height="26" HorizontalAlignment="Left" Margin="0,270,0,0"
Name="label2" VerticalAlignment="Top" Width="161" />
<ListBox Height="161" HorizontalAlignment="Left" Margin="5,294,0,0" Name="listBoxCourse"
VerticalAlignment="Top" Width="492"
ItemsSource="{Binding CourseList}">
<ListBox.ItemTemplate>
<DataTemplate >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding CourseId, StringFormat=Id:{0}}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding CourseName, StringFormat=Name:{0}}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Ellipse Height="31" HorizontalAlignment="Left" Margin="326,462,0,0" Name="ellipse1"
Stroke="Black" VerticalAlignment="Top" Width="166" >
<Ellipse.Style>
<Style>
<Setter Property="Ellipse.Fill" Value="Yellow" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsDataStreamFinished}" Value="true">
<Setter Property="Ellipse.Fill" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</Grid>
</Window>

As you might have noticed we have thrown in a couple of labels to identify the data loaded in the two list boxes. The difference is we have incorporated another list box to show the list of Courses. It is bound to CourseList in the view model. Each member of this collection should have two properties i.e. CourseId and CourseName.

As we described above the view model is expected to have a collection called CourseList and each member of this collection should be having two properties CourseId and CourseName. Let us see what updates we need to make in the view model.

class MainWindowViewModel : IObserver<Student>, INotifyPropertyChanged, IObserver<Course>
{
#region Properties

private ObservableCollection<StudentViewModel> _studentList;
public ObservableCollection<StudentViewModel> StudentList
{
get { return _studentList; }
set
{
_studentList = value;
onPropertyChanged("StudentList");
}
}

private ObservableCollection<CourseViewModel> _courseList;
public ObservableCollection<CourseViewModel> CourseList
{
get { return _courseList; }
set
{
_courseList = value;
onPropertyChanged("CourseList");
}
}

private bool _isDataStreamFinished;
public bool IsDataStreamFinished
{
get { return _isDataStreamFinished; }
set
{
_isDataStreamFinished = value;
onPropertyChanged("IsDataStreamFinished");
}
}

#endregion Properties

#region Observable Model
//Model
private StudentsModel model;
private CoursesModel courseModel;

#endregion Observable Model

IDisposable unsubscriber;

#region Constructor

public MainWindowViewModel()
{
_studentList = new ObservableCollection<StudentViewModel>();

model = new StudentsModel();

//subscribe to all updates
//model.Subscribe(this);

//subscribe to 10 updates after first 2 updates
//unsubscriber = model.Skip(2).Take<Student>(10).Subscribe(this);

//subscribe to all students whose Ids are even
//unsubscriber = (from m in model
// where m.StudentId % 2 == 0
// select m).Subscribe(this);

//observe on UI thread
unsubscriber = Observable.ObserveOnDispatcher<Student>(
(from m in model
where m.StudentId % 2 == 0
select m)
).Subscribe(this);

courseModel = new CoursesModel();
_courseList = new ObservableCollection<CourseViewModel>();
Observable.ObserveOnDispatcher<Course>(courseModel).Subscribe(this);

}

#endregion Constructor

#region IObserver implementation
public void OnCompleted()
{
//throw new NotImplementedException();
//unsubscriber.Dispose();
IsDataStreamFinished = true;
}

public void OnError(Exception error)
{
//throw new NotImplementedException();
}

public void OnNext(Student value)
{
//throw new NotImplementedException();
_studentList.Add(new StudentViewModel(value));
}

#endregion IObserver implementation

#region INotifyPropertyChanged implementation

public event PropertyChangedEventHandler PropertyChanged;
private void onPropertyChanged(string propertyname)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
}

#endregion INotifyPropertyChanged implementation


public void OnNext(Course value)
{
//throw new NotImplementedException();
_courseList.Add(new CourseViewModel(value));
}
}

You should notice that the view model is not implementing IObserver<Course>;. This would enable it to observe an IObservable<Course>. It has been introduced with one IObservable<Course> instance named courseModel. We have also subscribed to this in the constructor to observe the courseModel on Dispatcher. To implement the interface, we need to provide the definition of three methods, OnNext, OnCompleted and OnError. Since OnCompleted and OnError are already provided so we don't need to provide a new definition, even, we can not implement as this would be considered as overloading the methods and this requires the methods, being overloaded, to have different signatures. In the OnNext(Course) method, we are adding a new CourseViewModel to CourseList collection.

Now let us provide the definition CourseModel. This is an IObservable<Course>. This would require it to provide an implementation of Subscribe() method. This method should accept IObserver<Course> and should return an IDisposable. Disposing this returned object should result in the nullify the subscription. This definition is similar to StudensModel.

class CoursesModel : IObservable<Course>
{
private int _courseId;
//For updates on a non-UI thread
Timer t = new Timer(1000);

public List<IObserver<Course>> Observers =
new List<IObserver<Course>>();

public CoursesModel()
{
t.Elapsed += new ElapsedEventHandler(t_Elapsed);
t.Start();
}

//For updates on a non-UI thread
void t_Elapsed(object sender, ElapsedEventArgs e)
{
_courseId++;

var course = new Course()
{
CourseId = _courseId,
CourseName = string.Format("Course : {0}", _courseId)
};

if (_courseId <= 3)
{
Observers.ForEach((observer) => observer.OnNext(course));
}
else
{
Observers.ForEach((observer) => observer.OnCompleted());
t.Stop();
}
}

public IDisposable Subscribe(IObserver<Course> observer)
{
IDisposable unSubscriber = new Unsubscriber<Course>(Observers, observer);
if (!Observers.Contains(observer))
{
Observers.Add(observer);
}
return unSubscriber;
}
}

The definition of Course and CourseViewModel are as follows:

class Course
{
public string CourseName { get; set; }
public int CourseId { get; set; }
}

class CourseViewModel : INotifyPropertyChanged
{
public CourseViewModel(Course course)
{
this.CourseId = course.CourseId;
this.CourseName = course.CourseName;
}

#region Properties

private string _coursetName;
public string CourseName
{
get { return _coursetName; }
set
{
_coursetName = value;
OnPropertyChanged("CourseName");
}
}

private int _courseId;
public int CourseId
{
get { return _courseId; }
set
{
_courseId = value;
OnPropertyChanged("CourseId");
}
}

#endregion Properties

#region INotifyPropertyChanged implementation

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

#endregion INotifyPropertyChanged implementation
}

Now run the application. It is successfully loading the data as desired but there is a problem. The courseModel data is loaded first. This causes OnCompleted to be executed which fills the notification ellipse as green. So we are giving the notification to the user that the data is completely loaded even before it is completely loaded.



So, an IObserver can observe multiple IObservable (s) but we really can not distinguish between OnCompleted and OnError being executed because of which IObservable. We need to find out way to do that.

There is an easier solution to this problem. We can implement the interfaces explicitly. Let us assume that the notification is just about StudentList. The ellipse should turn green only when the StudentList is loaded completely. Let us implement IObserver<Course>.

class MainWindowViewModel : IObserver<Student>, INotifyPropertyChanged, IObserver<Course>
{
#region Properties

private ObservableCollection<StudentViewModel> _studentList;
public ObservableCollection<StudentViewModel> StudentList
{
get { return _studentList; }
set
{
_studentList = value;
onPropertyChanged("StudentList");
}
}

private ObservableCollection<CourseViewModel> _courseList;
public ObservableCollection<CourseViewModel> CourseList
{
get { return _courseList; }
set
{
_courseList = value;
onPropertyChanged("CourseList");
}
}

private bool _isDataStreamFinished;
public bool IsDataStreamFinished
{
get { return _isDataStreamFinished; }
set
{
_isDataStreamFinished = value;
onPropertyChanged("IsDataStreamFinished");
}
}

#endregion Properties

#region Observable Model
//Model
private StudentsModel model;
private CoursesModel courseModel;

#endregion Observable Model

IDisposable unsubscriber;

#region Constructor

public MainWindowViewModel()
{
_studentList = new ObservableCollection<StudentViewModel>();

model = new StudentsModel();

//subscribe to all updates
//model.Subscribe(this);

//subscribe to 10 updates after first 2 updates
//unsubscriber = model.Skip(2).Take<Student>(10).Subscribe(this);

//subscribe to all students whose Ids are even
//unsubscriber = (from m in model
// where m.StudentId % 2 == 0
// select m).Subscribe(this);

//observe on UI thread
unsubscriber = Observable.ObserveOnDispatcher<Student>(
(from m in model
where m.StudentId % 2 == 0
select m)
).Subscribe(this);

courseModel = new CoursesModel();
_courseList = new ObservableCollection<CourseViewModel>();
Observable.ObserveOnDispatcher<Course>(courseModel).Subscribe(this);

}

#endregion Constructor

#region IObserver implementation
public void OnCompleted()
{
//throw new NotImplementedException();
//unsubscriber.Dispose();
IsDataStreamFinished = true;
}

public void OnError(Exception error)
{
//throw new NotImplementedException();
}

public void OnNext(Student value)
{
//throw new NotImplementedException();
_studentList.Add(new StudentViewModel(value));
}

#endregion IObserver implementation

#region INotifyPropertyChanged implementation

public event PropertyChangedEventHandler PropertyChanged;
private void onPropertyChanged(string propertyname)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
}

#endregion INotifyPropertyChanged implementation


void IObserver<Course>.OnCompleted()
{
//throw new NotImplementedException();
}

void IObserver<Course>.OnError(Exception error)
{
//throw new NotImplementedException();
}

void IObserver<Course>.OnNext(Course value)
{
_courseList.Add(new CourseViewModel(value));
}
}

Now when CoursesModel calls OnCompleted on the observers the explicit implementation of the interface's OnCompleted gets called which currently no implementation (just an empty method). So the ellipse is still yellow.



After the data is completely loaded, StudentsModel, an IObservable<Student> calls OnCompleted which sets the variable bound to the trigger on the view to change the fill color of ellipse to green.



Download:

No comments: