I am creating a window that consists of dynamically created controls. I'm creating them using an ItemsControl:
<ItemsControl Grid.Row="0" Name="DynamicContent" ItemsSource="{Binding Path=EmbeddedInputControls}" ItemTemplateSelector="{Binding Path=EmbeddedInputControlsTemplateSelector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Where the ItemTemplateSelector
selects a DataTemplate
based on type from resources:
<Window.Resources>
<converters:ErrorBooleanToBrush x:Key="ErrorBooleanToBrush"/>
<DataTemplate x:Key="EmbeddedStringInput" DataType="embeddedInputDescriptors:StringEmbeddedInputDescriptor">
<StackPanel Orientation="Horizontal" Background="{Binding IsErrored, Converter={StaticResource ErrorBooleanToBrush}}">
<Label Content="{Binding LabelContent}"></Label>
<dxe:TextEdit EditValue="{Binding TextValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="{Binding IsReadOnly}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="EmbeddedIntegerInput" DataType="embeddedInputDescriptors:IntegerEmbeddedInputDescriptor">
<StackPanel Orientation="Horizontal" Name="IntStackPanel">
<Label Content="{Binding LabelContent}"></Label>
<dxe:SpinEdit Value="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="{Binding IsReadOnly}"
MinValue="{Binding MinValue}" MaxValue="{Binding MaxValue}" Validate="OnValidate" Tag="{Binding Self}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
This works fine for display but validation has proven to be a headache. The solution I've finally found uses code-behind and the OnValidate
event handler:
private void OnValidate(object sender, ValidationEventArgs e)
{
var dynamicInputElement=((FrameworkElement)sender).Tag as IEmbeddedInputDescriptor;
var errorContent = dynamicInputElement?.ErrorContent(e.Value);
if(!string.IsNullOrEmpty(errorContent))
{
e.IsValid = false;
e.ErrorContent = errorContent;
}
}
It's the only code-behind I've had to write and I'd like to get rid of it but I just can't see how to implement validation without it. The validation rules are going to be based on business logic so I can't have them in the XAML. I almost got a solution when I gave each DataTemplate a style whose members were obtained from the associated descriptor but it was a bit ugly:
{Binding IsErrored, Converter={StaticResource ErrorBooleanToBrush}}
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsErrored}" Value="True">
<Setter Property="StackPanel.Background" Value="LightCoral">
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
The objects used to derive the framework elements are:
internal interface IEmbeddedInputDescriptor
{
bool IsReadOnly { get; }
bool IsRequired { get; }
object Value { get; }
bool IsErrored { get; }
string ErrorContent(object value);
}
It'd be nice to get rid of the code-behind and I'm sure I'd learn more useful stuff about WPF in the process.
More info: Re binding validations:
<dxe:SpinEdit Value="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidationRules={Binding valRules}}"
and
public IEnumerable<ValidationRule> valRules
{
get { throw new NotImplementedException(); }
}
Results in the error 'A 'Binding' cannot be used within a 'ValidationRuleCollection' collection. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.'.
I had problems with IDataErrorInfo as well. I never managed to get either property to be called whether I implemented it in my main viewmodel or in the descriptors themselves. A possible problem should I get that far is how to identify the error property as I suspect it will always be 'Value' which is ambiguous. In fact that was the problem with most solutions the ones that yielded an event I could respond to left me with no context to work from.
That's why my current solution resorts to using the Tag property. Without that it's hard to link a form event back to the underlying business object.