Thursday, October 29, 2009

WPF Data Binding – ComboBox Items List

As I learn WPF I’m running into quite a few roadblocks, but I’m enjoying the fact that it’s a challenge and something new.  One of the things I’m trying to get used to is Data Binding to the WPF UI.  I’m also trying to learn WPF the MVVM way.  There are a lot of examples on how to do this on the web so I’ll just get to what got me stuck for a bit on ComboBox items binding.  Here’s some of my UI XAML and my code behind:

<DockPanel Margin="0" Height="42" Name="TopDockPanel" Width="auto" DockPanel.Dock="Top" LastChildFill="true">
<Grid Height="auto" Name="Grid1" Width="266" DockPanel.Dock="Left">
<Label Margin="7,7,0,9" HorizontalAlignment="Left" Width="71">Combo1:</Label>
<ComboBox ItemsSource="{Binding ComboList1}" Margin="84,9,22,13" SelectionChanged="Combo1_SelectionChanged" />
</Grid>
<Grid Height="auto" Name="Grid2" Width="281">

<Label HorizontalAlignment="Left" Margin="6,7,0,9" Width="89">Combo2:</Label>
<ComboBox ItemsSource="{Binding ComboList2}" Margin="101,9,20,13" IsEditable="False" />
</Grid>
<Grid Height="auto" Name="Grid3" Width="auto" />
</DockPanel>

Class SomeClass
Private _SomeClassVM As SomeClassViewModel = Nothing

Sub New()

' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_SomeClassVM = New SomeClassViewModel
Me.DataContext = _SomeClassVM
End Sub

Private Sub Combo1_SelectionChanged(ByVal sender As System.Object, ByVal e As System.Windows.Controls.SelectionChangedEventArgs)
_SomeClassVM.Combo1Changed(sender)

End Sub
End Class


Nothing complicated, a simple MVVMish implementation (ish because there’s event handler code in the code behind file, I’ll be refactoring this as I go on).  My View Model implements the INotifyPropertyChanged Interface so that the UI can be notified of any property changes and update any data bindings as needed.  My View Model also exposes a couple of Public Properties: ComboList1 and ComboList2 which are List (Of String) objects:

Public Class SomeClassViewModel
Implements INotifyPropertyChanged

Private _ComboList1 As List(Of String)
Private _ComboList2 As List(Of String)

Public ReadOnly Property ComboList1() As List(Of String)
Get
Return _ComboList1
End Get
End Property

Public ReadOnly Property ComboList2() As List(Of String)
Get
Return _ComboList2
End Get
End Property

Sub New()
_ComboList1 = New List(Of String)

For Each item As String In ItemsList 'this is just a list of strings from anywhere
_ComboList1.Add(item)
Next

OnPropertyChanged("SQLServerList")

_ComboList2 = New List(Of String)

End Sub

Public Sub Combo1Changed(ByVal sender As System.Object)
_ComboList2.Clear()

For Each item As String In SomeOtherList 'this is just list of string from another place
_ComboList2.Add(sqlDatabase)
Next

OnPropertyChanged("ComboList2")
End Sub

Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

Private Sub OnPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class


Now, I thought that clearing and adding items to my ComboList2 object would suffice in propagating change notifications up to the UI…WRONG.  It seems there’s more to it than just clearing/adding to the list and raising the PropertyChanged event.  This implementation only notified the UI that ComboList2 changed once at the first time I changed ComboBox1’s selected item instead of each time.  What gives?

After some fiddling I found that I couldn’t use the current _ComboList2 object reference.  I had to actually set the _ComboList2 object to a new List for the UI to update:

Public Sub Combo1Changed(ByVal sender As System.Object)
Dim NewList As New List(Of String)

For Each item As String In SomeOtherList 'this is just list of string from another place
NewList.Add(item)
Next

_ComboList2 = NewList
OnPropertyChanged("ComboList2")
End Sub

There’s a more elegant way to solve the issue I had, which is to use an ObservableCollection instead of List (Of String) objects:

Public Class SomeClassModel

Private _ComboList1 As ObservableCollection(Of String) = Nothing
Private _ComboList2 As ObservableCollection(Of String) = Nothing

Public ReadOnly Property ComboList1() As ObservableCollection(Of String)
Get
Return _ComboList1
End Get
End Property

Public ReadOnly Property ComboList2() As ObservableCollection(Of String)
Get
Return _ComboList2
End Get
End Property

Sub New()
_ComboList1 = New ObservableCollection(Of String)

For Each item As String In ItemsList 'this is just a list of strings from anywhere
_ComboList1.Add(item)
Next

_ComboList2 = New ObservableCollection(Of String)

End Sub

Public Sub Combo1Changed(ByVal sender As System.Object)
_ComboList2.Clear

For Each item As String In SomeOtherList 'this is just list of string from another place
_ComboList2.Add(item)
Next

End Sub
End Class

You’ll notice a couple of things.  The first being that the View Model no longer needs INotifyPropertyChanged.  This is because ObservableCollection implements INotifyPropertyChanged (and INotifyCollectionChanged).  You’ll also notice that I was able to just clear/add to _ComboList2 without having to raise any events.  The UI updated as expected and I suspect it’s because when clearing or adding items to the ObservableCollection, properties such as count raise the PropertyChanged event properly.

1 comment:

Greg said...

Hey Peter, another cheese MVVM sample is our Quickboard app.

http://sftsquickboard.codeplex.com/
There's also links to a number of WPF and MVVM resources that helped me a good bit...