Using a Grid as an ItemsHost - wpf

I would like to use a Grid as an ItemsHost but none of the items appear in their bound (column, row) positions. How can I make this work? As a simple example:
XAML
<ItemsControl ItemsSource="{Binding DataSet}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Grid.Column="{Binding Col}" Grid.Row="{Binding Row}" Text="{Binding Text}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Style>
<Style TargetType="{x:Type ItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Grid HorizontalAlignment="Stretch" IsItemsHost="True">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.Style>
</ItemsControl>
Codebehind
Class Window1
Private myTestData As TestData
Public Sub New()
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
myTestData = New TestData()
Me.DataContext = myTestData
End Sub
End Class
Class TestData
Private myDataSet As List(Of DataPoint)
Public Property DataSet() As List(Of DataPoint)
Get
Return myDataSet
End Get
Set(ByVal value As List(Of DataPoint))
myDataSet = value
End Set
End Property
Public Sub New()
Me.DataSet = New List(Of DataPoint)
For x As Integer = 0 To 1
For y As Integer = 0 To 1
Me.DataSet.Add(New DataPoint(x, y, "Column " + x.ToString + ", Row " + y.ToString))
Next
Next
End Sub
End Class
Class DataPoint
Private myRow As Integer
Public Property Row() As Integer
Get
Return myRow
End Get
Set(ByVal value As Integer)
myRow = value
End Set
End Property
Private myCol As Integer
Public Property Col() As Integer
Get
Return myCol
End Get
Set(ByVal value As Integer)
myCol = value
End Set
End Property
Private myText As String
Public Property Text() As String
Get
Return myText
End Get
Set(ByVal value As String)
myText = value
End Set
End Property
Public Sub New(ByVal x As Integer, ByVal y As Integer, ByVal name As String)
Me.Row = y
Me.Col = x
Me.Text = name
End Sub
End Class

Because you're using an ItemsControl, a container is generated for each item. That container (an instance of ContentPresenter for a plain old ItemsControl) wraps the TextBlock, and is a direct child of the Grid. Therefore, the Grid never even sees the Column and Row properties on the TextBlock because it's looking instead at the container.
You can solve this by setting an ItemContainerStyle that binds the appropriate properties for the container:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Grid.Column" Value="{Binding Column}"/>
<Setter Property="Grid.Row" Value="{Binding Row}"/>
</Style>
</ItemsControl.ItemContainerStyle>

Possible workaround: if you use UniformGrid, you may not need to specify the rows and columns.

Related

TextBox in WPF TreeView not updating source

I cannot tell if I fundamentally don't understand something here, or have just done something silly.
I have a tree view with two types of templates.
Nodes that are HierarchicalDataTemplate with a TextBlock
Leaves that are DataTemplate with a TextBlock and a TextBox3.
The binding from source is working fine in all cases and reading the Name or Name & Value properties from the underlying classes, and updating if from any changes from the code behind.
Both the TreeNode and TreeLeaf class implement INotifyPropertyChanged
However the binding of the TextBox.text property back to the TreeLeaf.Value property (with its getter) does not seem to work.
XAML
<TreeView Name="ItemsTree" Grid.Row="1" Grid.Column="1" ItemsSource="{Binding}">
<TreeView.Resources>
<Color x:Key="detailMark">#FFA1A9B3</Color>
<SolidColorBrush x:Key="detailMarkBrush" Color="{StaticResource ResourceKey=detailMark}" />
<Style x:Key="flatTextBox" TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Grid>
<Rectangle Stroke="{StaticResource ResourceKey=detailMarkBrush}" StrokeThickness="0"/>
<TextBox Margin="1" Text="{TemplateBinding Text}" BorderThickness="0"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<HierarchicalDataTemplate DataType="{x:Type local:TreeNode}" ItemsSource="{Binding Children, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}">
<TextBlock Text="{Binding Name, NotifyOnSourceUpdated=True}">
</TextBlock>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:TreeLeaf}" >
<StackPanel Orientation="Horizontal">
<TextBlock Width="150" Text="{Binding Name, NotifyOnSourceUpdated=True}"/>
<TextBox Width="150" Text="{Binding Value, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Style="{StaticResource ResourceKey=flatTextBox}"/>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
And then code behind for adding the items to the tree
ItemsTree.Items.Add(treeRoot)
The TreeLeaf class
Public Class TreeLeaf
Implements INotifyPropertyChanged, IEditableObject
Private m_Key As String
Private m_Value As String
Private m_Parent As TreeNode
Private temp_Item As TreeLeaf = Nothing
Private m_Editing As Boolean = False
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Sub New()
m_Key = ""
m_Value = ""
End Sub
Public Sub NotifyPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public Sub BeginEdit() Implements IEditableObject.BeginEdit
If Not m_Editing Then
temp_Item = MemberwiseClone()
m_Editing = True
End If
End Sub
Public Sub EndEdit() Implements IEditableObject.EndEdit
If m_Editing = True Then
temp_Item = Nothing
m_Editing = False
End If
End Sub
Public Sub CancelEdit() Implements IEditableObject.CancelEdit
If m_Editing = True Then
m_Key = temp_Item.m_Key
m_Value = temp_Item.m_Value
m_Editing = False
End If
End Sub
Public Property Parent As TreeNode
Get
Return m_Parent
End Get
Set(value As TreeNode)
m_Parent = value
NotifyPropertyChanged("Parent")
End Set
End Property
Public ReadOnly Property Name As String
Get
Return m_Key & " : "
End Get
End Property
Public Property Key As String
Get
Return m_Key
End Get
Set(ByVal value As String)
If Not value = m_Key Then
m_Key = value
NotifyPropertyChanged("Key")
End If
End Set
End Property
Public Property Value As String
Get
Return m_Value
End Get
Set(ByVal value As String)
If Not value = m_Value Then
m_Value = value
NotifyPropertyChanged("Value")
End If
End Set
End Property
End Class
I have seen mentions of setting the DataContext but I don't understand how that would apply to this situation.
Well, unfortunately figured it out.
The style template for the text box was overriding the binding.
The line with TemplateBinding seems to default to a one way from source
<TextBox Margin="1" Text="{TemplateBinding Text}" BorderThickness="0"/>
and it now works if you change it too
<TextBox Margin="1" Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}" BorderThickness="0"/>
It's not possible to tell from your code whether the binding works or not since the ViewModel code is complicated enough and also depends on the treeview viewmodel code etc...
One way to debug these types of problems is to create a Converter and make it part of the binding that should be debugged. Setting a breakpoint within the Converter will show whether the binding is called as expected when the value in the source or destination has changed. The code for the converter is realy simple:
Imports System
Imports System.Globalization
Imports System.Windows.Data
Namespace MyApp.Converters
Public Class DebugConverter
Inherits IValueConverter
Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object
Return value
End Function
Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object
Return value
End Function
End Class
End Namespace
Its integration into a questionable binding is also simple. Add it via its namespace (eg MyApp.Converters) into the XAML:
xmlns:conv="clr-namespace:MyApp.Converters"
...and create an instance via this statement in the Resource section of your TreeView code listing:
<conv:DebugConverter x:Key="DebugConverter"/>
...and last but not least integrate the converter into a binding you want to debug:
<TextBox
Width="150"
Style="{StaticResource ResourceKey=flatTextBox}"
Text="{Binding Value, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource DebugConverter}}" />
Now, when you set a breakpoint in the Convert and ConvertBack method you should see these methods being invoked when you start the application. If you do not see it in either direction you know the binding is either not working or the value is not being updated and you can then search further for the cause.
Hope this helps.

Treeview IsSelected with different viewmodel

I have a treeview binded to an Observablecollection of ParentViewModel.
The ParentViewModel contains an Observablecollection of ChildViewModel.
Both this ViewModel implement an IsSelected property. I put it in both of them because the implementation is a little bit different. The program have to behave differently if I press a Parent or Child item.
My problem is that also if I select a Child item, program execute the IsSelected property of the ParentViewModel, not the one of the ChildViewModel.
This is my XAML:
<TreeView Name="MyTreeView"
ItemsSource="{Binding ParentList}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:ParentViewModel}" ItemsSource="{Binding Childs}">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="{Binding PictureString}" Height="32" Width="32" Margin="0,8,6,4" />
<TextBlock Grid.Column="1" Text="{Binding Name}" FontSize="15" Margin="10" />
</Grid>
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:ChildViewModel}">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!--<Image Grid.Column="0" Source="{Binding PictureString}" Height="32" Margin="0,8,6,4" />-->
<TextBlock Grid.Column="1" Text="{Binding Name}" Height="18" FontSize="15" Margin="10"/>
</Grid>
</DataTemplate>
</TreeView.Resources>
</TreeView>
The program never hit the IsSelected setter of the ChildViewModel.
Thanks in advance
EDIT
This is the BaseViewModel:
Public Class InheritableTreeViewItem
Implements INotifyPropertyChanged
Friend m_Name As String
Public Property Name As String
Get
Return m_Name
End Get
Set(value As String)
If (value <> m_Name) Then
m_Name = value
NotifyPropertyChanged("Name")
End If
End Set
End Property
Friend m_IsSelected As Boolean
Public Overridable Property IsSelected As Boolean
Get
Return m_IsSelected
End Get
Set(value As Boolean)
If (value <> m_IsSelected) Then
m_IsSelected = value
NotifyPropertyChanged("IsSelected")
End If
End Set
End Property
Private m_sPictureString As String
Private m_Items As ObservableCollection(Of InheritableTreeViewItem)
Public Property PictureString As String
Get
Return m_sPictureString
End Get
Set(value As String)
If value <> m_sPictureString Then
m_sPictureString = value
NotifyPropertyChanged("PictureString")
End If
End Set
End Property
Public Property Items As ObservableCollection(Of InheritableTreeViewItem)
Get
Return m_Items
End Get
Set(value As ObservableCollection(Of InheritableTreeViewItem))
m_Items = value
End Set
End Property
Sub New(Name As String)
Me.Name = Name
Me.Items = New ObservableCollection(Of InheritableTreeViewItem)
End Sub
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Sub NotifyPropertyChanged(propName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propName))
End Sub
End Class
And this is the ParentViewModel:
Public Class ParentViewModel
Inherits InheritableTreeViewItem
Public Overrides Property IsSelected As Boolean
Get
Return m_IsSelected
End Get
Set(value As Boolean)
If (value <> m_IsSelected) Then
m_IsSelected = value
If value Then
' The function that I want to call When Parent item is selected in the tree
End If
NotifyPropertyChanged("IsSelected")
End If
End Set
End Property
Sub New(Name As String)
MyBase.New(Name)
Me.PictureString = "/Resources/TreeView/Folder.png"
End Sub
End Class
The ChildViewModel is basically identical, it differs only for the function that I call when it is selected. This function modify the content and visibility of some TextBoxes.
I think your problem is at this block :
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
Because it is a style, I'm not sure that the DataContext is properly inherited. Also why do you redefine this in the HierarchicalDataTemplate? I don't think you need to redefine this style for ChildViewModelItems.
Maybe you could debug your databinding in depth to see what's happening on this property : How to debug data binding.
Also, the plural of Child is Children, not Childs ;)

My Combobox is not populating

I am using a combo box within a datagrid, binding to a view model using WPF and MVVM. My combo box is not populating though the data bound item does have data. I'm not sure what I'm doing wrong. Any help would be appreciated.
My first datagrid populates just fine with the cross sectional data.
My second datagrid populates with the appropriate zone data. However, the combobox itemsource and selected cross section are not working in the second data grid. If I set breakpoints in the property for crosssections and selected cross sections, I get to the breakpoints. However, the data is not being populated.
I updated my code to reflect the changes suggested. The drop down entries populated just fine, as did a selected item. However, I noticed that I had properties with the same name in both the MainWindowViewModel and the Zone viewmodel class. So, I updated the Zone class as below:
Imports GalaSoft.MvvmLight
Public Class Zone
Inherits ViewModelBase
Private _zoneNumber As Integer
Private _selectedZoneCrossSection As CrossSection
Private _length As Double
Public Property ZoneNumber As Integer
Get
Return _zoneNumber
End Get
Set(value As Integer)
_zoneNumber = value
RaisePropertyChanged(Function() ZoneNumber)
End Set
End Property
Public Property SelectedZoneCrossSection As CrossSection
Get
Return _selectedZoneCrossSection
End Get
Set(value As CrossSection)
_selectedZoneCrossSection = value
RaisePropertyChanged(Function() SelectedZoneCrossSection)
End Set
End Property
Public Property Length As Double
Get
Return _length
End Get
Set(value As Double)
_length = value
RaisePropertyChanged(Function() Length)
End Set
End Property
End Class
I then modified the MainWindow.xaml ComboBox component as follows and now I'm working as I expected...
<DataGridComboBoxColumn Header="Cross Section"
SelectedValueBinding="{Binding SelectedZoneCrossSection}"
DisplayMemberPath="RecordNumber">
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.CrossSections}"/>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.CrossSections}"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
My MainWindow.Xaml
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ComboBoxBinding"
Title="MainWindow" Height="350" Width="800">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding CrossSections}"
SelectedItem="{Binding SelectedCrossSection}"
AutoGenerateColumns="False"
>
<DataGrid.Columns>
<DataGridTextColumn Header="No." Binding="{Binding RecordNumber}"/>
<DataGridTextColumn Header="Height" Binding="{Binding Height}"/>
<DataGridTextColumn Header="Width" Binding="{Binding Width}"/>
<DataGridTextColumn Header="Area" Binding="{Binding Area}"/>
</DataGrid.Columns>
</DataGrid>
<Separator/>
<DataGrid ItemsSource="{Binding Zones}" AutoGenerateColumns="True"
SelectedItem="{Binding SelectedZone}">
<DataGrid.Columns>
<DataGridTextColumn Header="No." Binding="{Binding ZoneNumber}"/>
<DataGridComboBoxColumn Header="Cross Section"
ItemsSource="{Binding CrossSections, Mode=OneWay, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True}"
SelectedItemBinding="{Binding SelectedCrossSection}"
DisplayMemberPath="RecordNumber"
/>
<DataGridTextColumn Header="Length" Binding="{Binding Length}"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
</Window>
My MainWindowViewModel.vb
Imports System.Collections.ObjectModel
Imports GalaSoft.MvvmLight
Public Class MainWindowViewModel
Inherits ViewModelBase
Private _crossSections As ObservableCollection(Of CrossSection) = New ObservableCollection(Of CrossSection)()
Private _selectedCrossSection As CrossSection
Private _zones As ObservableCollection(Of Zone) = New ObservableCollection(Of Zone)
Private _selectedZone As Zone
Public Property CrossSections As ObservableCollection(Of CrossSection)
Get
Return _crossSections
End Get
Set(value As ObservableCollection(Of CrossSection))
_crossSections = value
RaisePropertyChanged(Function() CrossSections)
End Set
End Property
Public Property SelectedCrossSection As CrossSection
Get
Return _selectedCrossSection
End Get
Set(value As CrossSection)
_selectedCrossSection = value
RaisePropertyChanged(Function() SelectedCrossSection)
End Set
End Property
Public Property Zones As ObservableCollection(Of Zone)
Get
Return _zones
End Get
Set(value As ObservableCollection(Of Zone))
_zones = value
RaisePropertyChanged(Function() Zones)
End Set
End Property
Public Property SelectedZone As Zone
Get
Return _selectedZone
End Get
Set(value As Zone)
_selectedZone = value
RaisePropertyChanged(Function() SelectedZone)
End Set
End Property
Public Sub New()
InitializeCrossSections()
InitializeZones()
End Sub
Public Sub InitializeCrossSections()
Dim oc As ObservableCollection(Of CrossSection) = New ObservableCollection(Of CrossSection)()
Dim cs As CrossSection
For i = 1 To 10
cs = New CrossSection()
cs.RecordNumber = i
cs.Height = i
cs.Width = i + 1
cs.Area = cs.Height * cs.Width
oc.Add(cs)
Next
CrossSections = New ObservableCollection(Of CrossSection)(oc)
SelectedCrossSection = CrossSections(0)
End Sub
Public Sub InitializeZones()
Dim oc As ObservableCollection(Of Zone) = New ObservableCollection(Of Zone)()
Dim zn As Zone
For i = 1 To 3
zn = New Zone()
zn.ZoneNumber = i
zn.Length = 10 - i * 0.25
zn.SelectedCrossSection = CrossSections(i)
oc.Add(zn)
Next
Zones = New ObservableCollection(Of Zone)(oc)
SelectedZone = Zones(0)
End Sub
End Class
My CrossSection Class:
Imports GalaSoft.MvvmLight
Public Class CrossSection
Inherits ViewModelBase
Private _recordNumber As Integer
Private _height As Double
Private _width As Double
Private _area As Double
Public Property RecordNumber As Integer
Get
Return _recordNumber
End Get
Set(value As Integer)
_recordNumber = value
RaisePropertyChanged(Function() RecordNumber)
End Set
End Property
Public Property Height As Double
Get
Return _height
End Get
Set(value As Double)
_height = value
RaisePropertyChanged(Function() Height)
End Set
End Property
Public Property Width As Double
Get
Return _width
End Get
Set(value As Double)
_width = value
RaisePropertyChanged(Function() Width)
End Set
End Property
Public Property Area As Double
Get
Return _area
End Get
Set(value As Double)
_area = value
RaisePropertyChanged(Function() Area)
End Set
End Property
End Class
My Zone Class
Imports GalaSoft.MvvmLight
Public Class Zone
Inherits ViewModelBase
Private _zoneNumber As Integer
Private _selectedCrossSection As CrossSection
Private _length As Double
Public Property ZoneNumber As Integer
Get
Return _zoneNumber
End Get
Set(value As Integer)
_zoneNumber = value
RaisePropertyChanged(Function() ZoneNumber)
End Set
End Property
Public Property SelectedCrossSection As CrossSection
Get
Return _selectedCrossSection
End Get
Set(value As CrossSection)
_selectedCrossSection = value
RaisePropertyChanged(Function() SelectedCrossSection)
End Set
End Property
Public Property Length As Double
Get
Return _length
End Get
Set(value As Double)
_length = value
RaisePropertyChanged(Function() Length)
End Set
End Property
End Class
I made few changes. Refer the below code.
<Grid>
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding CrossSections}"
SelectedItem="{Binding SelectedCrossSection}"
AutoGenerateColumns="False"
>
<DataGrid.Columns>
<DataGridTextColumn Header="No." Binding="{Binding RecordNumber}"/>
<DataGridTextColumn Header="Height" Binding="{Binding Height}"/>
<DataGridTextColumn Header="Width" Binding="{Binding Width}"/>
<DataGridTextColumn Header="Area" Binding="{Binding Area}"/>
</DataGrid.Columns>
</DataGrid>
<Separator/>
<DataGrid ItemsSource="{Binding Zones}" AutoGenerateColumns="False"
SelectedItem="{Binding SelectedZone}">
<DataGrid.Columns>
<DataGridTextColumn Header="No." Binding="{Binding ZoneNumber}"/>
<DataGridComboBoxColumn Header="Cross Section"
SelectedValueBinding="{Binding SelectedCrossSection}"
DisplayMemberPath="RecordNumber">
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.CrossSections}"/>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.CrossSections}"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
<DataGridTextColumn Header="Length" Binding="{Binding Length}"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>

IValueConverter is only called first time databound TreeView is displayed

I have a TreeView that is bound to an ObservableCollection (that properly implements IPropertyNotifyChanged). Each TreeViewItem is a HierarchicalDataTemplate. I have a Converter on the TextBlock that is bound to 'amount' - that changes the foreground colour to red if the string represents a negative number. The first time the TreeView is loaded the amounts all display correctly. However if the underlying ObservableCollection is changed then the amount changes correctly but the colour doesn't (ie a negative 'amount' is shown in white not red).
I have tried using both a IValueConverter and IMultiValueConverter. I have ensured everything is bound with UpdateSourceTrigger=PropertyChanged. The converter just isn't getting called.
What do I need to do to get the converter to be called every time the 'amount' changes??
Thanks
Andy
Template:
<!-- Data templates-->
<HierarchicalDataTemplate x:Key="RealTemplate" DataType="{x:Type l:Account}" ItemsSource="{Binding Path=children}">
<DockPanel LastChildFill="True">
<TextBlock x:Name="AccountTitle" Text="{Binding Path=title}" Foreground="White" DockPanel.Dock="Left"/>
<TextBox x:Name="EditAccountTitle" Text="{Binding Path=title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource RoundedTextBox}" FontWeight="Bold" LostFocus="tvLostFocus" PreviewKeyDown="tvKeyDown" LostKeyboardFocus="tvLostFocus" Visibility="Collapsed" DockPanel.Dock="Left" l:FocusExtension.IsFocused="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" CaretIndex="{x:Static sys:Int32.MaxValue}"/>
<TextBlock Text="{Binding Path=amount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" DockPanel.Dock="Right" TextAlignment="Right">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource GetColourConverterAmountM}" UpdateSourceTrigger="PropertyChanged">
<Binding/>
</MultiBinding>
</TextBlock.Foreground>
</TextBlock>
</DockPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=isEditable}" Value="True">
<Setter TargetName="AccountTitle" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="EditAccountTitle" Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=isEditable}" Value="False">
<Setter TargetName="AccountTitle" Property="Visibility" Value="Visible"/>
<Setter TargetName="EditAccountTitle" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource TreeViewTopItemConverter}}" Value="False">
<DataTrigger.Setters>
<Setter Property="ContextMenu" Value="{StaticResource RealAccountMenu}"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource TreeViewTopItemConverter}}" Value="True">
<DataTrigger.Setters>
<Setter Property="ContextMenu" Value="{StaticResource CategoryMenu}"/>
</DataTrigger.Setters>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
Converter:
Public Class GetColourConverterAmountM
Implements IMultiValueConverter
Function Convert(ByVal values() As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As Globalization.CultureInfo) As Object Implements IMultiValueConverter.Convert
If values Is Nothing Then
Return New SolidColorBrush(Colors.White)
ElseIf (Val(values(0).amount) >= 0) Then
Return New SolidColorBrush(Colors.White)
Else
Return New SolidColorBrush(Colors.Red)
End If
End Function
Function ConvertBack(ByVal value As Object, ByVal targetTypes() As Type, ByVal parameter As Object, ByVal culture As Globalization.CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack
Return Nothing
End Function
End Class
ObservableCollection:
Public Class Account
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged()
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(Nothing))
End Sub
Private _name, _title, _amount, _target As String
Private _ID, _type, _category, _column As Integer
Private _isEditable, _isNodeExpanded, _isNodeSelected As Boolean
Private _children As ObservableCollection(Of Account)
Public Sub New(__ID As Integer, __name As String, __title As String, __amount As String, __target As String, __type As Integer, __category As Integer, __column As Integer)
children = New ObservableCollection(Of Account)
_ID = __ID
_name = __name
_title = __title
_amount = __amount
_target = __target
_category = __category
_type = __type
_column = __column
_isEditable = False
_isNodeExpanded = True
_isNodeSelected = False
End Sub
Property ID As Integer
Get
Return _ID
End Get
Set(value As Integer)
_ID = value
NotifyPropertyChanged()
End Set
End Property
Property name As String
Get
Return _name
End Get
Set(value As String)
_name = value
NotifyPropertyChanged()
End Set
End Property
Property title As String
Get
Return _title
End Get
Set(value As String)
_title = value
NotifyPropertyChanged()
End Set
End Property
Property amount As String
Get
Return _amount
End Get
Set(value As String)
_amount = value
NotifyPropertyChanged()
End Set
End Property
Property target As String
Get
Return _target
End Get
Set(value As String)
_target = value
NotifyPropertyChanged()
End Set
End Property
Property category As Integer
Get
Return _category
End Get
Set(value As Integer)
_category = value
NotifyPropertyChanged()
End Set
End Property
Property type As Integer
Get
Return _type
End Get
Set(value As Integer)
_type = value
NotifyPropertyChanged()
End Set
End Property
Property column As Integer
Get
Return _column
End Get
Set(value As Integer)
_column = value
NotifyPropertyChanged()
End Set
End Property
Property isEditable As Boolean
Get
Return _isEditable
End Get
Set(value As Boolean)
_isEditable = value
NotifyPropertyChanged()
End Set
End Property
Property isNodeExpanded As Boolean
Get
Return _isNodeExpanded
End Get
Set(value As Boolean)
_isNodeExpanded = value
NotifyPropertyChanged()
End Set
End Property
Property isNodeSelected As Boolean
Get
Return _isNodeSelected
End Get
Set(value As Boolean)
_isNodeSelected = value
NotifyPropertyChanged()
End Set
End Property
Property children As ObservableCollection(Of Account)
Get
Return _children
End Get
Set(value As ObservableCollection(Of Account))
_children = value
NotifyPropertyChanged()
End Set
End Property
End Class
You need to bind TextBlock's Foreground property to amount, and use converter to convert amount to color. With that Foreground will be updated whenever amount value changed. For example (not using multibinding) :
<TextBlock Text="{Binding Path=amount, Mode=TwoWay}"
Foreground="{Binding Path=amount,
Converter="{StaticResource GetColourConverterAmount}"
DockPanel.Dock="Right" TextAlignment="Right">

WPF VB.NET Binding Image Source From String in Data Template

My first WPF application (please be gentle!), and I'm making an app based on events during a football match. So I've set up a Player class, and one of the properties is Nationality eg: Scotland. Then I have a DataTemplate, but rather than the string "Scotland" showing up, I would like an image of the national flag to show up instead. All my images are in Images/Countries then are named Scotland.png etc...
The line of code in my data template which I need the binding is:
<Image x:Name="Player_Nationality_Image" Margin="0,0,0,0" Grid.Column="4"Source="Images/Countries/Scotland.png" Height="14" Width="14"/>
Is there an easy way to change my class slightly and bind the image source based on this Nationality property alone? Do I need something in between to convert this?
Here is my Player.vb class:
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Public Class Player
Implements INotifyPropertyChanged
Private playerFirstNameValue As String
Private playerSurnameValue As String
Private playerShirtNameValue As String
Private playerSquadNumberValue As Integer
Private playerDOBValue As Date
Private playerPlaceOfBirthValue As String
Private playerNationalityValue As String
Private playerCategoryValue As Position
Private specialFeaturesValue As SpecialFeatures
Private teamValue As ObservableCollection(Of Team)
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Property FirstName() As String
Get
Return Me.playerFirstNameValue
End Get
Set(ByVal value As String)
Me.playerFirstNameValue = value
OnPropertyChanged("FirstName")
End Set
End Property
Public Property Surname() As String
Get
Return Me.playerSurnameValue
End Get
Set(ByVal value As String)
Me.playerSurnameValue = value
OnPropertyChanged("Surname")
End Set
End Property
Public Property ShirtName() As String
Get
Return Me.playerShirtNameValue
End Get
Set(ByVal value As String)
Me.playerShirtNameValue = value
OnPropertyChanged("ShirtName")
End Set
End Property
Public Property SquadNumber() As Integer
Get
Return Me.playerSquadNumberValue
End Get
Set(ByVal value As Integer)
Me.playerSquadNumberValue = value
OnPropertyChanged("SquadNumber")
End Set
End Property
Public Property StartDate() As Date
Get
Return Me.playerDOBValue
End Get
Set(ByVal value As Date)
Me.playerDOBValue = value
OnPropertyChanged("DateofBirth")
End Set
End Property
Public Property PlaceOfBirth() As String
Get
Return Me.playerPlaceOfBirthValue
End Get
Set(ByVal value As String)
Me.playerPlaceOfBirthValue = value
OnPropertyChanged("Surname")
End Set
End Property
Public Property Nationality() As String
Get
Return Me.playerNationalityValue
End Get
Set(ByVal value As String)
Me.playerNationalityValue = value
OnPropertyChanged("Nationality")
End Set
End Property
Public Property Position() As Position
Get
Return Me.playerCategoryValue
End Get
Set(ByVal value As Position)
Me.playerCategoryValue = value
OnPropertyChanged("Position")
End Set
End Property
Public Property SpecialFeatures() As SpecialFeatures
Get
Return Me.specialFeaturesValue
End Get
Set(ByVal value As SpecialFeatures)
Me.specialFeaturesValue = value
OnPropertyChanged("SpecialFeatures")
End Set
End Property
Public ReadOnly Property Team() As ReadOnlyObservableCollection(Of Team)
Get
Return New ReadOnlyObservableCollection(Of Team)(Me.teamValue)
End Get
End Property
Public Sub New(ByVal FirstName As String, ByVal Surname As String, ByVal ShirtName As String, ByVal PlaceOfBirth As String, ByVal NationalityImageSourceString As String, ByVal Category As Position, ByVal squadNumber As Integer, ByVal DOB As Date, ByVal specialFeatures As SpecialFeatures)
Me.playerFirstNameValue = FirstName
Me.playerSurnameValue = Surname
Me.playerShirtNameValue = ShirtName
Me.playerPlaceOfBirthValue = PlaceOfBirth
Me.playerNationalityValue = Nationality
Me.playerDOBValue = DOB
Me.playerSquadNumberValue = squadNumber
Me.playerCategoryValue = Category
Me.specialFeaturesValue = specialFeatures
Me.teamValue = New ObservableCollection(Of Team)()
End Sub
Protected Sub OnPropertyChanged(ByVal name As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
End Sub
End Class
Public Enum Position
GK
DF
MF
FW
End Enum
Public Enum SpecialFeatures
None
Yellow
Red
SubbedOff
SubbedOn
Highlight
End Enum
Data Template in Application.xaml:
<DataTemplate DataType="{x:Type src:Player}">
<Button Name="Player_Button" Style="{StaticResource Player}" Height="24" Width="320" Margin="0,2,0,0">
<Grid HorizontalAlignment="Center" Width="300">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"></ColumnDefinition>
<ColumnDefinition Width="210"></ColumnDefinition>
<ColumnDefinition Width="10"></ColumnDefinition>
<ColumnDefinition Width="30"></ColumnDefinition>
<ColumnDefinition Width="30"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Name="Player_Number_Text" Grid.Column="0" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=SquadNumber}" Margin="0,0,0,0" HorizontalAlignment="Left" />
<StackPanel Orientation="Horizontal" Grid.Column="1">
<TextBlock Name="Player_FirstName_Text" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=FirstName}" Margin="5,0,0,0" HorizontalAlignment="Left" />
<TextBlock Name="Player_Surname_Text" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=Surname}" Margin="5,0,0,0" HorizontalAlignment="Left" />
</StackPanel>
<Rectangle Name="Player_Card" Grid.Column="2"></Rectangle>
<TextBlock Name="Player_Position_Text" Grid.Column="3" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=Position}" Margin="5,0,0,0" />
<Image x:Name="Player_Nationality_Image" Margin="0,0,0,0" Grid.Column="4" Source="Images/Countries/Scotland.png" Height="14" Width="14" />
</Grid>
</Button>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=SpecialFeatures}">
<DataTrigger.Value>
<src:SpecialFeatures>Yellow</src:SpecialFeatures>
</DataTrigger.Value>
<DataTrigger.Setters>
<Setter Property="Fill" Value="Yellow" TargetName="Player_Card" />
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding Path=SpecialFeatures}">
<DataTrigger.Value>
<src:SpecialFeatures>Highlight</src:SpecialFeatures>
</DataTrigger.Value>
<Setter Property="Fill" Value="Blue" TargetName="Player_Card" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
You'll need to implement an IValueConverter.
Somewhere in your solution add the following class. (I highly recommend making a converter folder and dumping all converters to be used throughout the application in this folder so you can find and modify them easily down the track.)
Imports System.ComponentModel
Imports System.Windows.Data
Imports System.Windows.Media
Public Class StringToImageConverter
Implements IValueConverter
Public Sub New()
End Sub
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object _
Implements IValueConverter.Convert
Select Case value
Case "Scotland"
'
Return New ImageSourceConverter().ConvertFromString("/YOURSOLUTION;component/Images/Countries/Scotland.png")
'
Case "Australia"
'
Return New ImageSourceConverter().ConvertFromString("/YOURSOLUTION;component/Images/Countries/Australia.png")
'
Case Else
Return Nothing
End Select
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object _
Implements IValueConverter.ConvertBack
End Function
End Class
In Application.xaml place the following code.
<Form.Resources>
<my:StringToImageConverter x:Key="StringToImageConverter" />
<Form.Resources>
<Button Name="Player_Button" Style="{StaticResource Player}" Height="24" Width="320" Margin="0,2,0,0">
<Grid HorizontalAlignment="Center" Width="300">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"></ColumnDefinition>
<ColumnDefinition Width="210"></ColumnDefinition>
<ColumnDefinition Width="10"></ColumnDefinition>
<ColumnDefinition Width="30"></ColumnDefinition>
<ColumnDefinition Width="30"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Name="Player_Number_Text" Grid.Column="0" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=SquadNumber}" Margin="0,0,0,0" HorizontalAlignment="Left" />
<StackPanel Orientation="Horizontal" Grid.Column="1">
<TextBlock Name="Player_FirstName_Text" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=FirstName}" Margin="5,0,0,0" HorizontalAlignment="Left" />
<TextBlock Name="Player_Surname_Text" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=Surname}" Margin="5,0,0,0" HorizontalAlignment="Left" />
</StackPanel>
<Rectangle Name="Player_Card" Grid.Column="2"></Rectangle>
<TextBlock Name="Player_Position_Text" Grid.Column="3" FontFamily="Font/#Gotham Narrow Bold" Text="{Binding Path=Position}" Margin="5,0,0,0" />
<Image Source="{Binding Path=Nationality, Converter={StaticResource StringToImageConverter}}" Stretch="None" />
</Grid>
Hope this helps.

Resources