IValueConverter is only called first time databound TreeView is displayed - wpf

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">

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.

WPF: Keeping track of relative item positions in ItemsControl/ListBox

Please see the following code.
It creates a ListBox with five items. The selected item of the ListBox is colored in yellow, previous items (index below selected index) are colored in green and future items (index above selected index) are colored in red.
ItemViewModel.vb
Public Class ItemViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private _title As String
Private _isOld As Boolean
Private _isNew As Boolean
Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
If String.IsNullOrEmpty(propertyName) Then
Exit Sub
End If
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public Property Title As String
Get
Return _title
End Get
Set(value As String)
_title = value
Me.OnPropertyChanged()
End Set
End Property
Public Property IsOld As Boolean
Get
Return _isOld
End Get
Set(value As Boolean)
_isOld = value
Me.OnPropertyChanged()
End Set
End Property
Public Property IsNew As Boolean
Get
Return _isNew
End Get
Set(value As Boolean)
_isNew = value
Me.OnPropertyChanged()
End Set
End Property
End Class
MainViewModel:
Public Class MainViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private ReadOnly _items As ObservableCollection(Of ItemViewModel)
Private _selectedIndex As Integer
Public Sub New()
_items = New ObservableCollection(Of ItemViewModel)
_items.Add(New ItemViewModel With {.Title = "Very old"})
_items.Add(New ItemViewModel With {.Title = "Old"})
_items.Add(New ItemViewModel With {.Title = "Current"})
_items.Add(New ItemViewModel With {.Title = "New"})
_items.Add(New ItemViewModel With {.Title = "Very new"})
Me.SelectedIndex = 0
End Sub
Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
If String.IsNullOrEmpty(propertyName) Then
Exit Sub
End If
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public ReadOnly Property Items As ObservableCollection(Of ItemViewModel)
Get
Return _items
End Get
End Property
Public Property SelectedIndex As Integer
Get
Return _selectedIndex
End Get
Set(value As Integer)
_selectedIndex = value
Me.OnPropertyChanged()
For index As Integer = 0 To Me.Items.Count - 1
Me.Items(index).IsOld = (index < Me.SelectedIndex)
Me.Items(index).IsNew = (index > Me.SelectedIndex)
Next index
End Set
End Property
End Class
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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="200">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<ListBox ItemsSource="{Binding Items}" SelectedIndex="{Binding SelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsOld}" Value="True">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="True">
<Setter Property="Foreground" Value="Yellow" />
</DataTrigger>
<DataTrigger Binding="{Binding IsNew}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
This works like expected, but I don't like, that the ItemViewModel holds the properties IsOld and IsNew and that the MainViewModel is responsible for updating these properties. In my opinion that should be done by the ListBox, not by every view model that might be the DataContext for my ListBox.
I already tried to create two attached properties for ListBoxItem and bind to them (like I bound to IsSelected for the current item). But I couldn't figure out an event on which I update those attached properties.
Is using these attached properties the way to go? When and/or where do I update those attached properties?
I tried to attach to the ValueChanged event of the ItemsSource property of the ListBox to be able to attach to the CollectionChanged event of the underlying collection. But I failed getting the ListBoxItem for an item, since these containers are created asynchronously (so I assume). And since the ListBox uses a VirtualizingStackPanel by default, I wouldn't get a ListBoxItem for every item of my underlying collection anyway.
Please keep in mind that the collection of items I bind to is observable and can change. So the IsOld and IsNew properties have to be updated whenever the source collection itself changes, whenever the content of the source collection changes and whenever the selected index changes.
Or how else can I achieve what I like to achieve?
I didn't flag VB.net on purpose since the question doesn't have anything to do with VB.net and I'm fine with answers in C# as well.
Thank you.
One way you can achieve this is through an attached behavior. This allows you to keep the display behavior with the ListBox and away from your view-model, etc.
First, I created an enum to store the states of the items:
namespace WpfApp4
{
public enum ListBoxItemAge
{
Old,
Current,
New,
None
}
}
Next, I created an attached behavior class with two attached properties:
IsActive (bool) = Turns on the behavior for the ListBox
ItemAge (ListBoxItemAge) = Determines if an item should be displayed in Red, Yellow, Green, etc.
When IsActive is set to True on a ListBox, it will subscribe to the SelectionChanged event and will handle setting each ListBoxItems age.
Here is the code:
using System.Windows;
using System.Windows.Controls;
namespace WpfApp4
{
public class ListBoxItemAgeBehavior
{
#region IsActive (Attached Property)
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.RegisterAttached(
"IsActive",
typeof(bool),
typeof(ListBoxItemAgeBehavior),
new PropertyMetadata(false, OnIsActiveChanged));
public static bool GetIsActive(DependencyObject obj)
{
return (bool)obj.GetValue(IsActiveProperty);
}
public static void SetIsActive(DependencyObject obj, bool value)
{
obj.SetValue(IsActiveProperty, value);
}
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ListBox listBox)) return;
if ((bool) e.NewValue)
{
listBox.SelectionChanged += OnSelectionChanged;
}
else
{
listBox.SelectionChanged -= OnSelectionChanged;
}
}
private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = (ListBox) sender;
var selectedIndex = listBox.SelectedIndex;
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(selectedIndex), ListBoxItemAge.Current);
foreach (var item in listBox.ItemsSource)
{
var index = listBox.Items.IndexOf(item);
if (index < selectedIndex)
{
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.Old);
}
else if (index > selectedIndex)
{
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.New);
}
}
}
#endregion
#region ItemAge (Attached Property)
public static readonly DependencyProperty ItemAgeProperty =
DependencyProperty.RegisterAttached(
"ItemAge",
typeof(ListBoxItemAge),
typeof(ListBoxItemAgeBehavior),
new FrameworkPropertyMetadata(ListBoxItemAge.None));
public static ListBoxItemAge GetItemAge(DependencyObject obj)
{
return (ListBoxItemAge)obj.GetValue(ItemAgeProperty);
}
public static void SetItemAge(DependencyObject obj, ListBoxItemAge value)
{
obj.SetValue(ItemAgeProperty, value);
}
#endregion
}
}
The XAML looks something like this. This is just a simple example:
<ListBox
local:ListBoxItemAgeBehavior.IsActive="True"
ItemsSource="{Binding Data}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Old">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Current">
<Setter Property="Foreground" Value="Yellow" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="New">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I've created three DataTriggers that look for the value of the ListBoxItemAgeBehavior.ItemAge and then set the appropriate Foreground color. Since the attached property is set on the ListBoxItem, I'm doing a RelativeSource on the binding.
I hope this helps.

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>

Enable ValidationRule if checkbox is checked

I want to validate some controls, but only if my checkbox is checked. (in this example i only show one textbox to validate)
I tried to work with BindingGroup, but this means that the checkbox is validated too which gives me wrong results.
Now I try it with MultiBinding:
<CheckBox Name="chkUseLqv"
Grid.Row="6"
Grid.Column="0"
Margin="0,0,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="Use LQV"
IsChecked="{Binding LqvConfigurationEnabled}" />
[...]
<GroupBox Grid.Row="1"
Grid.RowSpan="6"
Grid.Column="4"
Margin="5"
Header="LQV Configuration"
IsEnabled="{Binding LqvConfigurationEnabled}">
<Grid>
<TextBox Name="txtLqvDatabaseServer"
Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Center"
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}">
<TextBox.Text>
<MultiBinding Converter="{Converters:LqvConfigurationMultiBindingConverter}" UpdateSourceTrigger="PropertyChanged">
<MultiBinding.ValidationRules>
<LocalValidationRules:LqvDatabaseServerValidator />
</MultiBinding.ValidationRules>
<Binding ElementName="chkUseLqv" Path="IsChecked" />
<Binding Path="LqvDatabaseServer" />
</MultiBinding>
</TextBox.Text>
</TextBox>
</Grid>
</GroupBox>
My Validator:
Imports System.Text.RegularExpressions
Namespace ValidationRules
Public Class LqvDatabaseServerValidator
Inherits ValidationRule
Public Overrides Function Validate(ByVal value As Object, ByVal cultureInfo As System.Globalization.CultureInfo) As System.Windows.Controls.ValidationResult
Dim useLqv = CType(value, Object()).OfType(Of Boolean).First()
Dim valueFromSource = CType(value, Object()).OfType(Of String).FirstOrDefault()
If useLqv Then
If String.IsNullOrEmpty(valueFromSource) Then
Return New ValidationResult(False, "This field is required!")
End If
If Not (
Regex.IsMatch(valueFromSource, "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") OrElse
Regex.IsMatch(valueFromSource, "^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$")
) Then
Return New ValidationResult(False, "Invalid input!")
End If
End If
Return ValidationResult.ValidResult
End Function
End Class
End Namespace
The converter:
Imports System.Windows.Markup
Namespace Converters
Public Class LqvConfigurationMultiBindingConverter
Inherits MarkupExtension
Implements IMultiValueConverter
Public Sub New()
End Sub
Private _orig As Object()
Public Function Convert(ByVal values() As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert
_orig = values
Return values.OfType(Of String).FirstOrDefault()
End Function
Public Function ConvertBack(ByVal value As Object, ByVal targetTypes() As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack
Return _orig
End Function
Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
Return New LqvConfigurationMultiBindingConverter()
End Function
End Class
End Namespace
But it doesn't work. Can you help me?
I did it this way:
Define a style in resources:
<Style x:Key="lqvDependingFields" TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate />
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=chkUseLqv, Path=IsChecked}" Value="true">
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource ValidationErrorTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
Have a checkbox:
<CheckBox Name="chkUseLqv"
Grid.Row="6"
Grid.Column="0"
Margin="0,0,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="Use LQV"
IsChecked="{Binding LqvConfigurationEnabled}" />
Use the style on your control:
<TextBox Name="txtLqvDatabaseServer"
Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Center"
DependencyProperties:AttachedWindowProperties.HasValidationError="{Binding LqvDatabaseServerValidationError}"
Style="{StaticResource lqvDependingFields}">
<TextBox.Text>
<Binding NotifyOnSourceUpdated="True"
Path="LqvDatabaseServer"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<LocalValidationRules:LqvDatabaseServerValidator ValidatesOnTargetUpdated="True" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Dependency Property:
#Region "HasValidationErrorProperty"
Public Shared ReadOnly HasValidationErrorProperty As DependencyProperty =
DependencyProperty.RegisterAttached("HasValidationError", GetType(Boolean), GetType(AttachedWindowProperties),
New FrameworkPropertyMetadata(False, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
Nothing, AddressOf CoerceHasValidationError))
Public Shared Function GetHasValidationError(ByVal d As DependencyObject) As Boolean
Return CBool(d.GetValue(HasValidationErrorProperty))
End Function
Public Shared Sub SetHasValidationError(ByVal d As DependencyObject, ByVal value As Boolean)
d.SetValue(HasValidationErrorProperty, value)
End Sub
Private Shared Function CoerceHasValidationError(ByVal d As DependencyObject, ByVal baseValue As Object) As Object
Dim result As Boolean = CBool(baseValue)
If BindingOperations.IsDataBound(d, HasValidationErrorProperty) Then
If GetHasErrorDescriptor(d) Is Nothing Then
Dim desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType)
desc.AddValueChanged(d, AddressOf OnHasErrorChanged)
SetHasErrorDescriptor(d, desc)
result = Validation.GetHasError(d)
End If
Else
If GetHasErrorDescriptor(d) IsNot Nothing Then
Dim desc As DependencyPropertyDescriptor = GetHasErrorDescriptor(d)
desc.RemoveValueChanged(d, AddressOf OnHasErrorChanged)
SetHasErrorDescriptor(d, Nothing)
End If
End If
Return result
End Function
Private Shared Sub OnHasErrorChanged(ByVal sender As Object, ByVal e As EventArgs)
Dim d As DependencyObject = TryCast(sender, DependencyObject)
If d IsNot Nothing Then
d.SetValue(HasValidationErrorProperty, d.GetValue(Validation.HasErrorProperty))
End If
End Sub
Private Shared ReadOnly HasErrorDescriptorProperty As DependencyProperty =
DependencyProperty.RegisterAttached("HasErrorDescriptor", GetType(DependencyPropertyDescriptor), GetType(AttachedWindowProperties))
Private Shared Function GetHasErrorDescriptor(ByVal d As DependencyObject) As DependencyPropertyDescriptor
Return CType(d.GetValue(HasErrorDescriptorProperty), DependencyPropertyDescriptor)
End Function
Private Shared Sub SetHasErrorDescriptor(ByVal d As DependencyObject, ByVal value As DependencyPropertyDescriptor)
d.SetValue(HasErrorDescriptorProperty, value)
End Sub
#End Region
Where LqvDatabaseServer and LqvDatabaseServerValidationErrors are properties in my ViewModel.

Using a Grid as an ItemsHost

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.

Resources