Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

WPF Multi-Data Type Control

Rate me:
Please Sign up or sign in to vote.
4.83/5 (16 votes)
14 Sep 2017CPOL3 min read 12.2K   217   17   2
A custom control that adapts to the datatype of the source.

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.

 

Image 1

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
QuestionPerformance Pin
Graeme_Grant14-Sep-17 20:29
mvaGraeme_Grant14-Sep-17 20:29 
AnswerRe: Performance Pin
Clifford Nelson15-Sep-17 4:28
Clifford Nelson15-Sep-17 4:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.