Monday, February 1, 2010

Using the Observable Object for Change Notification in Calculated/Dependent Properties

Extending on my previous post, how could the Observable be employed when there are dependent/calculated properties that rely on the shared data inside the observable object?

Imagine a situation where the UI contains a drop down list whose selected index is databound to an Observable Object. Next to the drop down list is an info box that is visible only when a value in the list is selected. The visibility of the info box will be bound to a calculated property on the ViewModel, whose value is dependent on the value in the observable object. Therefore, if the value in the observable object changes, the visibility value needs to change too, and the View notified.

In the simplest case where there is no shared data and an observable object is not used, the XAML for an app such as this may look like this):

<ComboBox
Width="150"
Height="22"
Margin="5"
VerticalAlignment="Top"
HorizontalAlignment="Left"
ItemsSource="{Binding Contacts}"
SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}"
ItemTemplate="{StaticResource _itemTemplate}"
/>
<StackPanel Orientation="Vertical" Visibility="{Binding IsConfirmSelectionVisible, Converter={StaticResource VisibleIfTrue}}">
<TextBlock Margin="5" Text="Is this the correct selection?" />
<TextBlock Margin="5" Text="{Binding SelectedContact.SelectedContact.ContactId}" />


The ViewModel may look like this:

public int SelectedIndex
{
get
{
return _selectedIndex;
}
set
{
_selectedIndex = value;
RaisePropertyChanged("SelectedIndex");
RaisePropertyChanged("IsConfirmSelectionVisible");
}
}

public bool IsConfirmSelectionVisible
{
get
{
return SelectedIndex != -1;
}
}



Nice and simple. However, what if the SelectedIndex was a shared value between multiple ViewModels? As described in the previous post, a good method for doing this is to implement an Observable. The XAML would then look like this (the selected index property of the ComboBox is altered to use an Observable Object:

<ComboBox
Width="150"
Height="22"
Margin="5"
VerticalAlignment="Top"
HorizontalAlignment="Left"
ItemsSource="{Binding Contacts}"
SelectedIndex="{Binding SelectedIndexObservable.SelectedIndex, Mode=TwoWay}"
ItemTemplate="{StaticResource _itemTemplate}"
/>
<StackPanel Orientation="Vertical" Visibility="{Binding IsConfirmSelectionVisible, Converter={StaticResource VisibleIfTrue}}">
<TextBlock Margin="5" Text="Is this the correct selection?" />
<TextBlock Margin="5" Text="{Binding SelectedContact.SelectedContact.ContactId}" />



And the ViewModel like this:

public int SelectedIndexObservable
{
get
{
return _selectedIndexObservable;
}
set
{
_selectedIndexObservable = value;
RaisePropertyChanged("SelectedIndexObservable");
}
}

public bool IsConfirmSelectionVisible
{
get
{
return SelectedIndex != -1;
}
}


The problem here now is that when the SelectedIndex property is updated, the change notification will no longer work, as the change is occuring inside the observable. Raising the property changed event for IsConfirmSelectionVisible in there will not propogate to the UI. The way I see it, there are two options:

Option 1: Move IsConfirmSelectionVisible to the Observable

The property IsConfirmSelectionVisible is moved to the observable and the XAML for the StatckPanel's Visibility property is updated accordingly. Although this results in a minimal amount of additional code it is unclean in that code specifically intended for a View has been relocated into a shared resource (the Observable).

Option 2: Listen for changes in the Observable and React

In this scenario, when the Observable is injected into the ViewModel via the constructor, the ViewModel attaches itself to it and listens for property changed events. The code might look like this:

public SelectionViewModel
(
ISelectionView selectionView,
ISelectedContactObservable selectedContactObservable
)
{
...
View = selectionView;
View.ViewModel = this;

...

// Listen for property change events on the selected contact,
// as this controls whether some parts of the UI are visible or hidden
selectedContactObservable.PropertyChanged += HandleSelectedContactPropertyChanged;
...
}


Inside the HandleSelectedContactPropertyChanged event, the event is examined and the ViewModel reacts accordingly to notify the View of any relevant changes. The code might look like this:

private void HandleSelectedContactPropertyChanged
(
object sender,
PropertyChangedEventArgs args
)
{
switch (args.PropertyName)
{
case "SelectedIndex":
RaisePropertyChanged("IsConfirmSelectionVisible");
break;
default:
break;
}
}


This option requires additional code and some event wire-up. However, it maintains a better separation of concern between the View specific code and the shared Observable implementation.

Conclusion

Despite the requirement for some additional code, Option 2 is my preferred choice due to the better separation of concerns.

A working simple example of this is available in this example Silverlight project.

No comments:

Post a Comment