Is it possible to bind WPF Combobox.SelectedValue to multiple ObjectDataProviders? - wpf

Trying to determine if it is possible to bind the SelectedValue of a ComboBox to the inputs of multiple ObjectDataProviders with XAMAL Bindings.
I looked at MultiBinding but that appears to be grouping multiple controls together, not exactly what I'm looking to day.
I'd like to be able to have the ComboBox (locations) change the TextBlock (deviance) which it does AND to call the ObjectDataProvider (CommentProvider) to update the TextBox (locationComments).
This is fairly straightforward in a code-behind but would prefer to not go this route as a learning experience.
XAMAL CODE
<Window.Resources>
<ObjectDataProvider x:Key="LocationProvider"
ObjectType="{x:Type srv:ServiceClient}"
IsAsynchronous="True"MethodName="GetAssignedLocations" />
<ObjectDataProvider
x:Key="DevianceProvider"
ObjectType="{x:Type srv:ServiceClient}"
IsAsynchronous="True" MethodName="GetPercentChange">
<ObjectDataProvider.MethodParameters>
<system:String>Location1</system:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider
x:Key="CommentProvider"
ObjectType="{x:Type srv:ServiceClient}"
IsAsynchronous="True"
MethodName="GetCommentByBusinessUnit">
<ObjectDataProvider.MethodParameters>
<system:String>Location1</system:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<ComboBox Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="locations" VerticalAlignment="Top" ItemsSource="{Binding Source={StaticResource LocationProvider}}"
DisplayMemberPath="BuName" SelectedValuePath="BuKey"
SelectionChanged="locations_SelectionChanged">
<ComboBox.SelectedValue>
<Binding Source="{StaticResource DevianceProvider}"
Path="MethodParameters[0]"
BindsDirectlyToSource="True"
Mode="OneWayToSource" />
</ComboBox.SelectedValue>
<TextBlock Name="deviance" Height="23" Margin="0,0,645,17" Width="40" Text="{Binding Source={StaticResource DevianceProvider}}" IsEnabled="False" />
<TextBox Height="23" Margin="0,0,181,17" Name="locationComments" Width="350" />

You're on the right track with the MultiBinding.
The key is to use a MultiValueCoverter in conjunction with the MultiBinding.
<MultiBinding Converter="{StaticResource Coverter_LocationMultiConverter}"
Mode="OneWayToSource">
<Binding Source="{StaticResource DevianceProvider}"
Path="MethodParameters[0]"
BindsDirectlyToSource="True"
Mode="OneWayToSource" />
<Binding Source="{StaticResource CommentProvider}"
Path="MethodParameters[0]"
BindsDirectlyToSource="True"
Mode="OneWayToSource" />
</MultiBinding>
Where we were binding to just one thing before, now we are binding it to both ObjectDataProviders. The key factor that lets us do this is the converter:
public class LocationMultiCoverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return new object[] { value, value };
}
#endregion
}
Because we just need the same value in both places the CovertBack method is quite simple, however I'm sure you can see that it could be used to parse some complex stuff and pass back different components to different places in the UI.
Using this converter we can also try out a small sample, using two text boxes instead:
<Window x:Class="Sample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Sample"
Title="Window1"
Height="300"
Width="300">
<Window.Resources>
<local:LocationMultiCoverter x:Key="Coverter_LocationMultiConverter" />
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock x:Name="uiDeviance" />
<TextBlock x:Name="uiComment" />
<ComboBox x:Name="uiLocations"
Height="23"
HorizontalAlignment="Left"
VerticalAlignment="Top"
SelectedValuePath="Content">
<ComboBoxItem>1</ComboBoxItem>
<ComboBoxItem>2</ComboBoxItem>
<ComboBoxItem>3</ComboBoxItem>
<ComboBoxItem>4</ComboBoxItem>
<ComboBoxItem>5</ComboBoxItem>
<ComboBox.SelectedValue>
<MultiBinding Converter="{StaticResource Coverter_LocationMultiConverter}"
Mode="OneWayToSource">
<Binding ElementName="uiDeviance"
Path="Text"
BindsDirectlyToSource="True" />
<Binding ElementName="uiComment"
Path="Text"
BindsDirectlyToSource="True" />
</MultiBinding>
</ComboBox.SelectedValue>
</ComboBox>
</StackPanel>
</Grid>
(The Converter in my example exists in the Window's code behind as a separate class)
And as you can see testing this out it will update both TextBoxes when the SelectedValue changes.

Related

Binding child nodes to CollectionViewSource

So here is my data structure:
MyViewModel
Docs (ObservableCollection<Doc>)
Specs (ObservableCollection<Spec>)
i.e. the ViewModel has an ObservableCollection named Docs that is a collection of Doc objects, and in turn each Doc object has a collection of Spec objects. A property named position (available in both Doc and Spec classes) stores the logical position of each doc/spec.
I now need to bind this structure to a TreeView. I need to keep both both Docs and Specs sorted at all times (TreeView supports drag-n-drop rearrangement of nodes), so a direct binding cannot work here.
Therefore I use a CollectionViewSource to perform sorting at runtime.
<CollectionViewSource x:Key="DocumentsCVS" Source="{Binding Docs}">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription PropertyName="position" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
and use it in my TreeView:
<TreeView ItemsSource="{Binding Source={StaticResource DocumentsCVS}}">
So far so good. The TreeView shows my Docs sorted.
But from here onward things become confusing. How/where do I create CollectionViewSource for my specs? I tried doing this in my HierarchicalDataTemplate:
<HierarchicalDataTemplate>
<HierarchicalDataTemplate.ItemsSource>
<Binding>
<Binding.Source>
<CollectionViewSource Source="{Binding Specs}">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription PropertyName="position" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Binding.Source>
</Binding>
</HierarchicalDataTemplate.ItemsSource>
</HierarchicalDataTemplate>
But this doesn't work. Only Docs are listed in the TreeView, with no children inside. My gut feeling is that CollectionViewSource probably doesn't live in the same DataContext as the parent TreeViewItem.
Or is this something else?
Edit
Here is the full XAML of my TreeView:
<TreeView ItemsSource="{Binding Source={StaticResource DocumentsCVS}}"
PresentationTraceSources.TraceLevel="High">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type vm:DocumentVM}">
<HierarchicalDataTemplate.ItemsSource>
<Binding>
<Binding.Source>
<CollectionViewSource Source="{Binding Specs}">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription PropertyName="position" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Binding.Source>
</Binding>
</HierarchicalDataTemplate.ItemsSource>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate DataType="{x:Type vm:SpecVM}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Icon="PuzzlePiece" Width="16" Margin="3,3,6,3" Foreground="Orange" />
<Label Grid.Column="1" Content="{Binding name}" />
</Grid>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Icon="FileWordOutline" Height="16" Margin="3,3,6,3" Foreground="Crimson" />
<Label Grid.Column="1" Content="{Binding name}" />
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<CollectionViewSource Source="{Binding Specs}">
The Binding on Source there doesn't know where to go look for a DataContext (no "framework mentor"). I've kicked this around a bit. I can't find a place to define the CollectionViewSource where it inherits the DataContext from the template.
I did find a solution.
C#
public class SortConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var view = CollectionViewSource.GetDefaultView(value);
view.SortDescriptions.Add(new SortDescription((string)parameter, ListSortDirection.Ascending));
return view;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
<HierarchicalDataTemplate
DataType="{x:Type vm:DocumentVM}"
ItemsSource="{Binding Specs, Converter={StaticResource SortConverter}, ConverterParameter=position}"
>
This could be made more useful by giving the converter multiple PropertyName/SortDirection properties, or a collection of SortDescriptions. You could make it a MarkupExtension. You could also just create the collection view in a viewmodel property.

WPF Binding Calculated Values

I am trying to understand how ValueConverters work. I have three Text Boxes being txtQty, txtPrice and txtAmount representing Qty, Price and Amount respectively and Amount = Qty x Price.
txtQty and txtPrice are unbound controls whilst txtAmount is bound to a DataTable in a DataSet.
How can I update the value in txtAmount which is bound to a DataTable using ValueConveter which takes txtQty and txtPrice as input values?
I can easily achieve this in many ways. But I want to use a ValueConverter for this.
Any ideas?
You can create a converter that implements IMultiValueConverter to compute for your Price and Qty.
public class AmountConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
decimal qty = 0;
decimal price = 0;
if (values?.Length < 2)
throw new ArgumentNullException("Parameter should contain 2 values");
if (!string.IsNullOrEmpty(values[0].ToString()) && !decimal.TryParse(values[0].ToString(), out qty))
throw new ArgumentException("1st value should be decimal.");
if (!string.IsNullOrEmpty(values[1].ToString()) && !decimal.TryParse(values[1].ToString(), out price))
throw new ArgumentException("2nd value should be decimal.");
return (qty * price).ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then use MultiBinding for your Amount textbox
<TextBox x:Name="txtAmount" HorizontalAlignment="Left" IsReadOnly="True">
<TextBox.Text>
<MultiBinding Converter="{StaticResource AmountConverter}">
<Binding ElementName="txtQty" Path="Text" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"/>
<Binding ElementName="txtPrice" Path="Text" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
However you may have to do some interactivity with your txtQty and txtPrice to update your viewmodel-bound Amount, you may also need to invoke a command from your vm to accomplish this.
Listing the entire test xaml and viewmodel code...
<Window x:Class="WpfApp2.MainWindow"
x:Name="root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:interactivity="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp2"
xmlns:vm="clr-namespace:WpfApp2.ViewModel"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<vm:ViewModelTest />
</Window.DataContext>
<Window.Resources>
<local:AmountConverter x:Key="AmountConverter" />
</Window.Resources>
<Grid Margin="12 0 0 0" >
<StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock HorizontalAlignment="Left" Text="Qty" Margin="0 0 12 0" />
<TextBox x:Name="txtQty" HorizontalAlignment="Left" Height="20" Width="50" >
<interactivity:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding DataContext.UpdateAmountCommand, ElementName=root}" CommandParameter="{Binding Path=Text, ElementName=txtAmount}" />
</i:EventTrigger>
</interactivity:Interaction.Triggers>
</TextBox>
</StackPanel>
<StackPanel Orientation="Vertical" >
<TextBlock HorizontalAlignment="Left" Text="Price" Margin="0 0 12 0" />
<TextBox x:Name="txtPrice" HorizontalAlignment="Left" Height="20" Width="50" >
<interactivity:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding DataContext.UpdateAmountCommand, ElementName=root}" CommandParameter="{Binding Path=Text, ElementName=txtAmount}" />
</i:EventTrigger>
</interactivity:Interaction.Triggers>
</TextBox>
</StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock HorizontalAlignment="Left" Text="Amount" Margin="0 0 12 0" />
<TextBox x:Name="txtAmount" HorizontalAlignment="Left" IsReadOnly="True">
<TextBox.Text>
<MultiBinding Converter="{StaticResource AmountConverter}">
<Binding ElementName="txtQty" Path="Text" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"/>
<Binding ElementName="txtPrice" Path="Text" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</Grid>
</Window>
VM
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace WpfApp2.ViewModel
{
public class ViewModelTest : INotifyPropertyChanged
{
public ViewModelTest()
{
UpdateAmountCommand = new CustomCommand<string>(UpdateAmount, (x) => true);
}
private decimal _amount;
public decimal Amount
{
get => _amount;
set
{
if (_amount != value)
{
_amount = value;
OnPropertyChanged();
}
}
}
public CustomCommand<string> UpdateAmountCommand { get; }
private void UpdateAmount(string amountText)
{
Amount = decimal.Parse(amountText);
Debug.WriteLine(Amount);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Not sure if there's an easier way, this is just on top of my head.
PS: You can copy the CustomCommand implementation here.
Hope this helps.

XAML Databinding in Control templates via a dynamic Xpath expression

Ok, I'm pretty new to XAML & WPF, so I'm trying a learning project to construct a periodic table. I have a set of elemental data in XML. I have defined a control template in XAML to display this information for each chemical element as a button in a grid:
<ControlTemplate x:Key="elementTemplate" TargetType="{x:Type Button}">
<Grid>
<Rectangle x:Name="backingRect" Margin="2" StrokeThickness="0" RadiusX="3" RadiusY="3" Fill="{TemplateBinding Property=Button.Background}">
<Rectangle.Effect>
<DropShadowEffect BlurRadius="8"/>
</Rectangle.Effect>
</Rectangle>
<Viewbox>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.2*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="0.2*"/>
</Grid.RowDefinitions>
<TextBlock FontWeight="DemiBold" TextAlignment="Center" Grid.Row="0" diag:PresentationTraceSources.TraceLevel="High">
<TextBlock.Text>
<Binding Source="{StaticResource elementData}">
<Binding.XPath>atom[#id='H']/scalar[#dictRef='bo:atomicNumber']</Binding.XPath>
</Binding>
</TextBlock.Text>
</TextBlock>
<TextBlock FontWeight="Bold" TextAlignment="Center" FontSize="28" Grid.Row="1" >
<TextBlock.Text>
<Binding Source="{StaticResource elementData}" >
<Binding.XPath>atom[#id='H']/label[#dictRef='bo:symbol']/#value</Binding.XPath>
</Binding>
</TextBlock.Text></TextBlock >
<TextBlock FontWeight="DemiBold" TextAlignment="Center" Grid.Row="2">1.0021</TextBlock>
</Grid>
</Viewbox>
</Grid>
</ControlTemplate>
Now, this will create a button which binds to my underlying XML data statically. What I want to do is to replace the #id='H' XPath criterion with an expression that retrieves the Content of the Button that uses this template such as #id={TemplateBinding Button.Content} (if such a thing were possible), so if I create such a Button with a textual content of 'He', it will retrieve the corresponding data in the XML file such as mass, atomic number etc. What is the syntax for specifying such a dynamic XPath in XAML and binding it to a property on the control template?
See http://www.codeproject.com/Articles/854564/Parameterized-XPaths-in-XAML for a fuller discussion, ...
Essentially you have to use multi-binding with a multi-binding converter that performs the parameterized path replacement as the binding is being constructed and then return the desired content.
E.g.
<MultiBinding Converter="{StaticResource XPathConverter}" ConverterParameter="atom[#id="{1}"]/scalar[#dictRef='bo:atomicNumber']" Mode="OneWay">
<Binding Source="{StaticResource elementData}" Mode="OneWay" />
<Binding ElementName="_Button" Path="Content" Mode="OneWay"/>
</MultiBinding>
If you only need a OneWay binding, the converter is straightforward. The ConverterParamenter is the XPath with a parameterized placeholder(s). The first binding is to your XML data. And the second binding is for your control data to be used for the parameterized replacement. You will have to correct the converter below for your specific data objects and your desired return value.
public class XPathConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
XmlNode root = (value[0] as XmlNode);
string xpathf = (parameter as string);
for (int i = 1; i < values.Count(); i++)
xpathf = xpathf.Replace("{" + i.ToString().Trim() + "}", values[i] as string ?? "");
return root.SelectSingleNode(xpathf).InnerXML;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Expand/Collapse all expanders

I have a list of expanders which I want to control its expanded state(IsExpanded) with global toggle button which should toggle between expanded/collapsed state.
The solution which I have got so far does that by binding the expander's IsExpanded state to the togglebutton's IsChecked state. This works as long as I dont manually dont toggle the expanders. Once I do that those particular expanders dont respect the binding (toggle button's IsChecked state).
Any idea why? and is there a clean solution in XAML for this?
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<StackPanel>
<ToggleButton Name="ExpandAll">Toggle</ToggleButton>
<Expander IsExpanded="{Binding ElementName=ExpandAll,Path=IsChecked, Mode=OneWay}">
Hai
</Expander>
<Expander IsExpanded="{Binding ElementName=ExpandAll,Path=IsChecked, Mode=OneWay}">
Hello
</Expander>
<Expander IsExpanded="{Binding ElementName=ExpandAll,Path=IsChecked, Mode=OneWay}">
Weird
</Expander>
</StackPanel>
</Grid>
</Page>
I know that this post is very old. Just posting this for anyone else who comes across. The below code worked for me.
<Expander IsExpanded="{Binding ElementName=ExpandAll, Path=IsChecked, UpdateSourceTrigger=Explicit}">
</Expander>
This works when the expanders are generated dynamically, for example inside a DataGrid.RowDetailsTemplate.
I don't think you can achieve this entirely in XAML, but the following allows you to do it with an IValueConverter:
<StackPanel>
<StackPanel.Resources>
<local:Converter x:Key="Converter" />
</StackPanel.Resources>
<ToggleButton Name="ExpandAll">
<ToggleButton.IsChecked>
<MultiBinding Mode="OneWayToSource" Converter="{StaticResource Converter}">
<Binding ElementName="Expander1" Path="IsExpanded" />
<Binding ElementName="Expander2" Path="IsExpanded" />
<Binding ElementName="Expander3" Path="IsExpanded" />
</MultiBinding>
</ToggleButton.IsChecked>
Toggle</ToggleButton>
<Expander Name="Expander1">
Hai
</Expander>
<Expander Name="Expander2">
Hello
</Expander>
<Expander Name="Expander3">
Weird
</Expander>
</StackPanel>
And your Converter is as below:
public class Converter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//we're using OneWayToSource, so this will never be used.
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
// we want to convert the single 'IsChecked' value from the ToggleButton into
// 3 'IsExpanded' values
var allValues = new object[targetTypes.Length];
for (int i = 0; i < allValues.Length; i++)
{
allValues[i] = value;
}
return allValues;
}
}
This works by setting up a OneWayToSource binding between the IsChecked property of the ToggleButton (i.e. the binding will be set against the source when the target value changes), and uses an IMultiValueConverter to translate the single value into one for each of the Expanders.

How do I expand a control in WPF to make room for error messages in an ErrorTemplate?

I have a WPF window that uses validation. I created an error template that puts a red border around an element that fails validation and displays the error message below. This works fine, but the error message is rendered on top of any controls beneath the control with the error. The best I can tell, this happens because the error template renders on the Adorner Layer, which is on top of everything else. What I'd like to have happen is for everything else to move down to make room for the error message. Is there a way to do this? All of the examples on the web seem to use a tool tip and use a simple indicator like an asterisk or exclamation point that doesn't use much room.
Here is the template:
<ControlTemplate x:Key="ValidationErrorTemplate">
<StackPanel>
<Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
<AdornedElementPlaceholder x:Name="placeholder"/>
</Border>
<TextBlock Foreground="Red" FontSize="10" Text="{Binding ElementName=placeholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent, FallbackValue=Error!}"></TextBlock>
</StackPanel>
</ControlTemplate>
Here are the controls using the template (I typed some of this out, so ignore any syntax errors):
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Name="Account" Grid.Row="0" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}" Width="200">
<TextBox.Text>
<Binding Path="AccountNumber">
<Binding.ValidationRules>
<validators:RequiredValueValidationRule/>
<validators:NumericValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox Name="Expiration" Grid.Row="1" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}" Width="100" Margin="0,2,5,2">
<TextBox.Text>
<Binding Path="ExpirationDate">
<Binding.ValidationRules>
<validators:ExpirationDateValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</Grid>
EDIT: Alright, I'm not positive that this is the best solution (I sure hope someone can provide a better one), but here it goes:
Instead of using the Validation.ErrorTeplate, which will present all of the visuals in the AdornerLayer, you can add some TextBlocks and bind them to Validation.HasError and (Validation.Errors)[0].ErrorContent, using a customer IValueConverter to convert the Validation.HasError bool to a Visibility value. It would look something like the following:
Window1.cs:
<Window x:Class="WpfApplicationTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:WpfApplicationTest"
Title="Window1" Height="300" Width="300">
<Grid Margin="10">
<Grid.Resources>
<!-- The person we are binding to -->
<local:Person x:Key="charles" Name="Charles" Age="20" />
<!-- The convert to use-->
<local:HasErrorToVisibilityConverter x:Key="visibilityConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- The name -->
<TextBox Name="NameTextBox" Grid.Row="0" Text="{Binding Source={StaticResource charles}, Path=Name, ValidatesOnDataErrors=true}" />
<TextBlock Grid.Row="1"
Foreground="Red"
Text="{Binding ElementName=NameTextBox, Path=(Validation.Errors)[0].ErrorContent}"
Visibility="{Binding ElementName=NameTextBox, Path=(Validation.HasError), Converter={StaticResource visibilityConverter}}" />
<!-- The age -->
<TextBox Name="AgeTextBox" Grid.Row="2" Text="{Binding Source={StaticResource charles}, Path=Age, ValidatesOnExceptions=true}" />
<TextBlock Grid.Row="3"
Foreground="Red"
Text="{Binding ElementName=AgeTextBox, Path=(Validation.Errors)[0].ErrorContent}"
Visibility="{Binding ElementName=AgeTextBox, Path=(Validation.HasError), Converter={StaticResource visibilityConverter}}" />
</Grid>
</Window>
Person.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Text.RegularExpressions;
namespace WpfApplicationTest
{
public class Person : IDataErrorInfo
{
public string Name { get; set; }
public int Age { get; set; }
#region IDataErrorInfo Members
string IDataErrorInfo.Error
{
get { throw new NotImplementedException(); }
}
string IDataErrorInfo.this[string columnName]
{
get
{
switch (columnName)
{
case ("Name"):
if (Regex.IsMatch(this.Name, "[^a-zA-Z ]"))
{
return "Name may contain only letters and spaces.";
}
else
{
return null;
}
default:
return null;
}
}
}
#endregion
}
}
HasErrorToVisibilityConverter.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows;
namespace WpfApplicationTest
{
[ValueConversion(typeof(bool), typeof(Visibility))]
public class HasErrorToVisibilityConverter : IValueConverter
{
#region IValueConverter Members
object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool hasError = (bool)value;
return hasError ? Visibility.Visible : Visibility.Collapsed;
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
It doesn't scale as well as having a single ControlTemplate that you can reference in all of your controls, but it's the only solution I've found. I feel your pain - just about every example I can find on the topic of WPF validation is very simple, and almost always uses '!' or '*' preceding the control, with a tooltip bound to (Validation.Errors)[0].ErrorContent...
Best of luck to ya! If I find a better solution, I'll update this ;)

Resources