Introduction
I had a case where there were different data type for attributes used in setting up tests. Originally there were separate templates for each test, but I thought this was too much to maintain, so went to a single template, and instead of each type having its own classes, went to a generic appoach with each test type using the same class, and the data being in a colleciton of Attributes which depended on a generic class.
The Design
The following is the XAML for this ContentControl
<Style TargetType="{x:Type local:VariableTypeControl}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:VariableTypeControl}">
<Grid>
<TextBox x:Name="PART_TextBox"
MinWidth="{Binding MinWidth,
RelativeSource={RelativeSource TemplatedParent}}"
Text="{Binding Value,
RelativeSource={RelativeSource TemplatedParent}}" />
<TextBox x:Name="PART_TimeSpanTextBox"
Width="100"
HorizontalAlignment="Left"
local:TimeSpanTextBoxBehaviour.MaxTime="24:00:00"
local:TimeSpanTextBoxBehaviour.TimeFormat="Seconds"
local:TimeSpanTextBoxBehaviour.Value="{Binding Value,
RelativeSource={RelativeSource TemplatedParent}}" />
<ComboBox x:Name="PART_ComboBox"
MinWidth="{Binding MinWidth,
RelativeSource={RelativeSource TemplatedParent}}"
SelectedItem="{Binding Value,
RelativeSource={RelativeSource TemplatedParent}}" />
<ToggleButton x:Name="PART_OnOffButton"
Width="50"
HorizontalAlignment="Left"
Background="#01000000"
BorderBrush="Gray"
Style="{StaticResource {x:Static ToolBar.ToggleButtonStyleKey}}">
<TextBlock x:Name="PART_OnOffButtonTextBox"
FontWeight="Bold" />
</ToggleButton>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
There are three controls in this ContentControl
:
TextBox
used for data types such as strings and numbers.
TextBox
that uses a TimeSpanControlBehavior to support the TimeSpan type.
ComboBox
to support enumerations types, and cases where a ItemsSource
is specified
ToggleButton
to suppot Boolean or Yes/No data
Currently the code does not handle all data types, but does include support for a ItemsSource
DependencyProperty
. It the ItemsSource
DependencyProperty
it not null, then a ComboBox
is the control that is Visible
. It should be noted that the FrameworkPropertyMetadata
is used instead of the PropertyMetadata
for the Value
DependencyProperty
so the that the FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
can be applied.
The data types currently supported are:
- Selection list using the
ComboBox
that can be of any type as long as the ItemsSource
is not null.
- Enumbertions using the
ComboBox
where the enumeraton type is used to create the ItemsSource
for the ComboBox
- Boolean using the
ToggleButton
with the caption "No" in red for false and "Yes" in green for true.
TimeSpan
using a TextBox
with an attached behavior.
- String using a
TextBox
- 32 and 64 bit integers using a
TextBox
but adding event handlers (OnTextInput
, OnPreviewKeyDown
) to ensure that the key presses are for numbers, and a PastingHandler
that will ensure the same.
The C# code to support this is as follows:
[TemplatePart(Name = TextBoxName, Type = typeof(TextBox))]
[TemplatePart(Name = ComboBoxName, Type = typeof(ComboBox))]
[TemplatePart(Name = OnOffButtonName, Type = typeof(ToggleButton))]
[TemplatePart(Name = OnOffButtonTextBoxName, Type = typeof(TextBlock))]
[TemplatePart(Name = TimeSpanTextBoxName, Type = typeof(TextBox))]
public class VariableTypeControl : ContentControl
{
private const string TextBoxName = "PART_TextBox";
private TextBox _textBox;
private const string ComboBoxName = "PART_ComboBox";
private ComboBox _comboBox;
private const string OnOffButtonName = "PART_OnOffButton";
private ToggleButton _onOffButton;
private const string OnOffButtonTextBoxName = "PART_OnOffButtonTextBox";
private TextBlock _onOffButtonTextBox;
private const string TimeSpanTextBoxName = "PART_TimeSpanTextBox";
private TextBox _timeSpanTextBox;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_textBox = (TextBox)GetTemplateChild(TextBoxName);
_comboBox = (ComboBox)GetTemplateChild(ComboBoxName);
_onOffButton = (ToggleButton)GetTemplateChild(OnOffButtonName);
_onOffButtonTextBox = (TextBlock)GetTemplateChild(OnOffButtonTextBoxName);
_timeSpanTextBox = (TextBox)GetTemplateChild(TimeSpanTextBoxName);
SetNewValue(Value);
_onOffButton.Unchecked += OnOffButtonUnchecked;
_onOffButton.Checked += OnOffButtonChecked;
}
private void OnOffButtonUnchecked(object sender, RoutedEventArgs e)
{
_onOffButtonTextBox.Foreground = new SolidColorBrush(Colors.Red);
_onOffButtonTextBox.Text = "Off";
Value = false;
}
private void OnOffButtonChecked(object sender, RoutedEventArgs e)
{
_onOffButtonTextBox.Foreground = new SolidColorBrush(Colors.Lime);
_onOffButtonTextBox.Text = "On";
Value = true;
}
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(object),
typeof(VariableTypeControl), new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged));
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var valueTypeControl = (VariableTypeControl)d;
valueTypeControl.SetNewValue(e.NewValue);
}
public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable),
typeof(VariableTypeControl), new PropertyMetadata(null));
private Type _variableType;
private void SetNewValue(object newValue)
{
if (_textBox == null || newValue == null) return;
if (ItemsSource != null)
{
_textBox.Visibility = Visibility.Collapsed;
_onOffButton.Visibility = Visibility.Collapsed;
_timeSpanTextBox.Visibility = Visibility.Collapsed;
_comboBox.Visibility = Visibility.Visible;
_comboBox.HorizontalAlignment = HorizontalAlignment.Left;
_comboBox.ItemsSource = ItemsSource;
}
else if (_variableType != newValue?.GetType())
{
_textBox.Visibility = Visibility.Collapsed;
_onOffButton.Visibility = Visibility.Collapsed;
_timeSpanTextBox.Visibility = Visibility.Collapsed;
_comboBox.Visibility = Visibility.Collapsed;
if (newValue is Enum)
{
_comboBox.Visibility = Visibility.Visible;
_comboBox.HorizontalAlignment = HorizontalAlignment.Left;
_comboBox.ItemsSource = Enum.GetValues(newValue.GetType());
}
else if (newValue is bool)
{
_onOffButton.Visibility = Visibility.Visible;
_onOffButtonTextBox.Text = ((bool)newValue) ? "On" : "Off";
_onOffButton.IsChecked = (bool)newValue;
_onOffButtonTextBox.Foreground = new SolidColorBrush(((bool)newValue)
? Colors.Lime : Colors.Red);
}
else if (newValue is TimeSpan)
{
_timeSpanTextBox.Visibility = Visibility.Visible;
}
else if (ItemsSource != null)
{
_comboBox.Visibility = Visibility.Visible;
_comboBox.HorizontalAlignment = HorizontalAlignment.Left;
_comboBox.ItemsSource = ItemsSource;
}
else
{
IfIntType(newValue?.GetType());
if (newValue is string)
{
_textBox.Visibility = Visibility.Visible;
_textBox.HorizontalAlignment = HorizontalAlignment.Stretch;
_textBox.TextAlignment = TextAlignment.Left;
}
if (newValue is Int32 || newValue is Int64)
{
_textBox.Visibility = Visibility.Visible;
_textBox.HorizontalAlignment = HorizontalAlignment.Left;
_textBox.TextAlignment = TextAlignment.Right;
_comboBox.Visibility = Visibility.Collapsed;
}
}
if (newValue != null)
_variableType = newValue.GetType();
}
}
private void IfIntType(Type type)
{
if (type == typeof(Int32) || type == typeof(Int64))
{
PreviewTextInput += OnTextInput;
PreviewKeyDown += OnPreviewKeyDown;
DataObject.AddPastingHandler(_textBox, OnPaste);
}
else
{
PreviewTextInput -= OnTextInput;
PreviewKeyDown -= OnPreviewKeyDown;
DataObject.RemovePastingHandler(_textBox, OnPaste);
}
}
private void OnTextInput(object sender, TextCompositionEventArgs e)
{
if (e.Text.Any(c => !char.IsDigit(c))) { e.Handled = true; }
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space) e.Handled = true;
}
private static void OnPaste(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(DataFormats.Text))
{
var text = Convert.ToString(e.DataObject.GetData(DataFormats.Text)).Trim();
if (text.Any(c => !char.IsDigit(c))) { e.CancelCommand(); }
}
else
{
e.CancelCommand();
}
}
}
There is only a DependencyProperty
for the Value
and one for the ItemsSource
. Currently only the change in the Value
is handled with the static OnValueChanged
method, which calls the SetNewValue
method. This method basically aborts if the TextBox reference is null or the NewValue is null since until the OnApplyTemplate
has not been executed there is nothing to set, and if the NewValue
is null there is really nothing that can be done (may have to fix this for some usages but I have not had an issue). This method saves the Type
of the NewValue
argument, then makes sure the right Control
properties are set such as setting Visibility
to Visible
or Collapsed
. Since OnApplyTemplate
can happen after the Value and ItemsSource have been set, it not only gets references to the controls and sets the event
handlers for the Boolean ToggleButton
Checked
and Unchecked
events, but it also calls the SetNewValue
method to intialize the control.
If you look at the project you will notice that I included the name Template in the name for the XAML file since naming it the same as the associated C# code causes problems.
The Sample
The sample has has a control for each type that the ContentControl
handles. The following shows how this is used in the XAML:
<StackPanel Width="200"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<local:VariableTypeControl Margin="5,4"
VerticalAlignment="Stretch"
Value="{Binding StringValue}" />
<local:VariableTypeControl Margin="5,4"
VerticalAlignment="Stretch"
Value="{Binding EnumValue}" />
<local:VariableTypeControl Margin="5,4"
VerticalAlignment="Stretch"
Value="{Binding BoolValue}" />
<local:VariableTypeControl Margin="5,4"
VerticalAlignment="Stretch"
Value="{Binding TimeSpanValue}" />
<local:VariableTypeControl Width="100"
Margin="5,4"
VerticalAlignment="Stretch"
Value="{Binding IntValue}" />
<local:VariableTypeControl Margin="5,4"
VerticalAlignment="Stretch"
ItemsSource="{Binding ListItemsSource}"
Value="{Binding ListValue}" />
</StackPanel>
The ViewModel is as follows:
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
StringValue = "Test";
EnumValue = TestEnum.First;
BoolValue = true;
TimeSpanValue = new TimeSpan(5, 5, 5);
IntValue = (int)23;
ListValue = ListItemsSource.First();
}
private object _EnumValue;
private object _BoolValue;
private object _StringValue;
private object _IntValue;
private object _TimeSpanValue;
private object _ListValue;
public object StringValue
{
get => _StringValue;
set
{
_StringValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StringValue)));
}
}
public object ListValue
{
get => _ListValue;
set
{
_ListValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ListValue)));
}
}
public List<string> ListItemsSource
{
get => new List<string> { "First Item", "Second Item", "Third Item" };
}
public object IntValue
{
get => _IntValue;
set
{
_IntValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IntValue)));
}
}
public object TimeSpanValue
{
get => _TimeSpanValue;
set
{
_TimeSpanValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TimeSpanValue)));
}
}
public object BoolValue
{
get => _BoolValue;
set
{
_BoolValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BoolValue)));
}
}
public object EnumValue
{
get => _EnumValue;
set
{
_EnumValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EnumValue)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public enum TestEnum { First, Second, Third }
All the properties are of Type object just to show that do not need to have the right type.
Conclusion
This is a very useful control that has allowed me to significantly simplify the XAML in my project. It works for what I am working on, but still could be some issues which will need to be resolved, It also will not handle all the date types, but have what I need for the project, and will probably update this in the future as I use if for other projects.
History
- 09/14/2017: Initial Version, and fix for code error