My .NET 4.0 WPF application has a dialog box which contains a ListBox
. The ListBox
is initially empty. The dialog has UI controls that allow the user to insert items into the ListBox
. I'm using MVVM and the view model class implements IDataErrorInfo
. There is an ObservableCollection
in the view model class which is bound to the ListBox's ItemsSource
property.
I don't want to let the user click OK until they've entered at least one item into the ListBox
. If the ListBox
is empty, I want to display a red border around it & an error icon to the right of it, with the error message in the ListBox's
tooltip. This is the way all other errors are displayed in my application.
Here's an edited version of the XAML:
<Window x:Class="MyDialog class's type"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cs="clr-namespace:MyControlsDLL's namespace"
xmlns:vm="clr-namespace:My View Model DLL's namespace"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
DataContext="{Binding Path=SelectedCamera, RelativeSource={RelativeSource Self}}"
Loaded="MyDialog_Loaded"
SizeToContent="Height"
cs:ThemeSelector.CurrentThemeDictionary="{Binding Path=TimeOfDayTheme, RelativeSource={RelativeSource Self}}"
Width="800"
WindowStartupLocation="CenterOwner">
<cs:MyDialog.CommandBindings>
<cs:DataContextCommandBinding CanExecute="CanAddOrEditItem" Command="cs:MyCommands.AddItem" Executed="AddOrEditItem" />
<cs:DataContextCommandBinding CanExecute="CanRemoveItem" Command="cs:MyCommands.RemoveItem" Executed="RemoveItem" />
</cs:MyDialog.CommandBindings>
<cs:MyDialog.Resources>
<ResourceDictionary>
<BitmapImage x:Key="ErrorImage" UriSource="Resources/Error.png" />
<cs:BooleanInverter x:Key="NOT" />
<cs:BooleanToVisibilityConverter x:Key="BoolToVisibile" True="Visible" False="Collapsed" />
<cs:BooleanToVisibilityConverter x:Key="BoolToCollapsed" True="Collapsed" False="Visible" />
<cs:EnumToBooleanConverter x:Key="SubTypeConverter" />
<cs:MultiBoolConverter x:Key="BoolCombiner" />
<cs:EnumDisplayer ResourceManager="{x:Static res:MyApp.ResourceManager}" Type="{x:Type sys:DateTimeKind}" x:Key="DateTimeKinds">
<cs:EnumOverride EnumValue="Local" ResourceKey="DateKind_Local" />
<cs:EnumOverride EnumValue="Unspecified" ResourceKey="DateKind_Unspecified" />
<cs:EnumOverride EnumValue="Utc" ResourceKey="DateKind_Utc" />
</cs:EnumDisplayer>
<cs:EnumDisplayer ResourceManager="{x:Static res:MyApp.ResourceManager}" Type="{x:Type vm:MyTypes}" x:Key="MyTypes" />
<ControlTemplate x:Key="InputErrorTemplate">
<DockPanel LastChildFill="True">
<Image DockPanel.Dock="Right"
Height="20"
Margin="-30,0,0,0"
Source="{StaticResource ErrorImage}"
ToolTip="{x:Static res:MyApp.Common_InvalidData}"
VerticalAlignment="Center"
Width="20" />
<Border BorderBrush="Red"
BorderThickness="5"
Margin="5,5,30,5">
<AdornedElementPlaceholder />
</Border>
</DockPanel>
</ControlTemplate>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource InputErrorTemplate}" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<Binding Path="(Validation.Errors).CurrentItem.ErrorContent" RelativeSource="{x:Static RelativeSource.Self}" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource InputErrorTemplate}" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<Binding Path="(Validation.Errors).CurrentItem.ErrorContent" RelativeSource="{x:Static RelativeSource.Self}" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</cs:MyDialog.Resources>
<Grid Name="LayoutRoot" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<AdornerDecorator Grid.Row="0">
<Grid Name="CommonRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Column1" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Column2" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0"
Margin="5"
Text="Name:"
TextAlignment="Right"
VerticalAlignment="Center" />
<TextBox Grid.Column="1"
Grid.Row="0"
Margin="5,5,30,5"
MaxLength="50"
Name="NameBox"
TabIndex="0"
Text="{Binding Mode=TwoWay, NotifyOnSourceUpdated=True, Path=Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
TextAlignment="Left"
VerticalAlignment="Center" />
<TextBlock Grid.Column="3"
Grid.Row="0"
Margin="5"
Text="Type:"
TextAlignment="Right"
VerticalAlignment="Center" />
<ComboBox Grid.Column="4"
Grid.Row="0"
Height="50"
ItemsSource="{Binding Source={StaticResource MyTypes}, Path=DisplayNames}"
SelectedValue="{Binding Converter={StaticResource MyTypes}, Mode=TwoWay, Path=MyType}"
SelectionChanged="LPRTypePicker_SelectionChanged"
Margin="5"
x:Name="MyTypePicker"
TabIndex="1"
VerticalAlignment="Center" />
<TabControl Grid.Column="0"
Grid.ColumnSpan="6"
Grid.Row="1"
Name="Tabs">
<TabItem Header="Tab 1"
Name="Tab1">
<AdornerDecorator>
<Border BorderBrush="{DynamicResource ELDControlBorder}"
BorderThickness="0,0,0,1">
<Grid Background="{DynamicResource ELDContentBackground}"
Name="LPRConfiguration">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Column1" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Column2" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<!-- Row definitions -->
</Grid.RowDefinitions>
<!-- Controls for this TabItem -->
</Grid>
</Border>
</AdornerDecorator>
</TabItem>
<TabItem Header="Directories List"
Visibility="{Binding Path=CanShow, Converter={StaticResource BoolToVisibile}}">
<AdornerDecorator>
<Border BorderThickness="0,0,0,1">
<Grid Name="Tab2Root">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" SharedSizeGroup="Column1" />
<ColumnDefinition Width="3.5*" />
<ColumnDefinition Width="110" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Right"
Margin="5"
Text="Mode:" />
<Grid Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<RadioButton Content="SubType 1"
Grid.Column="1"
GroupName="SubTypes"
IsChecked="{Binding Converter={StaticResource SubTypeConverter}, ConverterParameter=SubType1, Path=SubType}"
Margin="5"
TabIndex="8"
VerticalAlignment="Center" />
<RadioButton Checked="SubType_Changed"
Content="Value2"
Grid.Column="3"
GroupName="SubTypes"
IsChecked="{Binding Converter={StaticResource SubTypeConverter}, ConverterParameter=SubType2, Path=SubType}"
Margin="5"
Name="SubTypeSubType2Button"
TabIndex="8"
VerticalAlignment="Center" />
</Grid>
<TextBlock Grid.Column="0"
Grid.Row="2"
HorizontalAlignment="Right"
Margin="5"
Text="An Item:"
VerticalAlignment="Center"
Visibility="{Binding Converter={StaticResource BoolToVisibile}, Path=SubTypeIsSubType1}" />
<Grid Grid.Column="1"
Grid.Row="2"
Visibility="{Binding Converter={StaticResource BoolToVisibile}, Path=SubTypeIsSubType1}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Margin="5"
Name="NewItemBox"
TabIndex="11"
Text="{Binding Mode=TwoWay, Path=NewItem, UpdateSourceTrigger=PropertyChanged}"
ToolTip="{x:Static res:MyApp.MyDialog_NewItem_Tooltip}"
VerticalAlignment="Center" />
<Button Command="cs:MyCommands.Browse"
Content="Browse..."
Grid.Column="1"
Height="35"
Margin="5"
Name="BrowseDirectoriesButton"
TabIndex="12" />
</Grid>
<Button Command="cs:MyCommands.AddItem"
Content="{Binding Path=AddOrEditLabel}"
Grid.Column="2"
Grid.Row="2"
Height="50"
Margin="5"
Name="AddItemButton"
TabIndex="13" />
<TextBlock Grid.Column="0"
Grid.Row="3"
HorizontalAlignment="Right"
Margin="5"
Text="Selected Items:"
TextAlignment="Right"
TextWrapping="WrapWithOverflow"
Visibility="{Binding Converter={StaticResource BoolToVisibile}, Path=SubTypeIsSubType1}" />
<ListBox Grid.Column="1"
Grid.Row="3"
Height="160"
ItemsSource="{Binding Path=SelectedItems}"
Margin="5,5,30,5"
Name="ItemsList"
SelectedValue="{Binding Mode=TwoWay, Path=SelectedItem, ValidatesOnDataErrors=True}"
TabIndex="14"
Visibility="{Binding Converter={StaticResource BoolToVisibile}, Path=SubTypeIsSubType1}" />
<Button Command="cs:MyCommands.RemoveItem"
Content="Remove"
Grid.Column="2"
Grid.Row="3"
Height="50"
Margin="5"
Name="RemoveItemButton"
TabIndex="15"
VerticalAlignment="Top"
Visibility="{Binding Converter={StaticResource BoolToVisibile}, Path=SubTypeIsSubType1}" />
</Grid>
</Border>
</AdornerDecorator>
</TabItem>
</TabControl>
</Grid>
</AdornerDecorator>
<!-- OK & Cancel Buttons here -->
</Grid>
</Window>
There's a complication in here in that even though this ListBox
is initially empty, it's only an error for it to be empty if the Type property is one value & the SubType property is SubType1. Initially, those properties do not have those values. When the dialog is first displayed, I can see my validation code getting called, but no error is displayed because the ListBox
isn't even being displayed.
After I change the Type property, the SubType property is automatically set to SubType1 & the ListBox
is displayed and it is empty. I've been able to verify that the validation code in my View Model class's IDataErrorInfo
implementation is being called, and an error message is returned, but no error template is displaying. Actually, no errors are being displayed on that tab at all, and I think it's identical to the tab before it, which is displaying errors.
What did I do wrong with the ListBox
?
EDIT
There is a TextBox
on that Tab
that also is initially in an error state & no error message was being displayed. When I put some text in it, the validation code ran & it was marked OK. When I emptied it, however, it did show an error condition. Maybe my problem is that I need to do something to put it into an error state when the user chooses Type1 or changes the SubType?
I got it to work. I added ValidatesOnDataErrors=True
to the binding for the ItemsSource
property on the ListBox
and now the error template is displaying.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments