Monday, November 3, 2014

WPF Customized ComboBox with Non Existing Value



How do you show a non existing value inside a WPF ComboBox? Good question! And the sad truth is that you just cannot do it! WPF ComboBox shows only the values that are in its ItemSource. 

But don't worry, today I'll show you a workaround to this problem. In this particular example inside a ComboBox will be shown a phantom value and when user clicks on it and choose another value from the list he will be prompted to confirm in order to give him a chance not to loose this phantom value in case if he clicked on the ComboBox by mistake.

WPF Customized ComboBox with Phantom Value

The trick is that I create additional custom ComboBox with TextBlock, binding it to the original one and overlaying it.

Check out the main window XAML:
<ComboBox x:Name="OriginalComboBox"
          Grid.Column="1" Grid.Row="3"
          VerticalAlignment="Top"
          ItemsSource="{Binding Employees}"
          SelectedValue="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

<phantom:PhantomValueComboBoxUserControl Grid.Column="1" Grid.Row="3"
                                      SelectedOptionText="{Binding Selected, Mode=OneWay}"
                                      RelatedComboBox="{Binding ElementName=OriginalComboBox}"/>

Now let's see the XAML of the Phantom ComboBox:

<UserControl x:Class="PhantomComboBoxExample.PhantomValueComboBoxUserControl.PhantomValueComboBoxUserControl"
      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:phantom="clr-namespace:PhantomComboBoxExample.PhantomValueComboBoxUserControl"
      mc:Ignorable="d" >
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition  Height="Auto" />
            <RowDefinition  Height="Auto" />
        </Grid.RowDefinitions>
        <ComboBox x:Name="MainComboBox"
               HorizontalAlignment="Stretch"
               ToolTipService.ToolTip="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
                     AncestorType=phantom:PhantomValueComboBoxUserControl}, 
                     Path=PhantomToolTip}" />

        <TextBlock IsHitTestVisible="False"
               x:Name="Watermark"
               Visibility="{Binding ElementName=MainCombo, Path=SelectedIndex}"
               Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
                    AncestorType=phantom:PhantomValueComboBoxUserControl}, 
                    Path=SelectedOptionText}"
               FontStyle="Italic"
               Foreground="DarkSlateGray"
               Padding="5,4,25,2"
               HorizontalAlignment="Left"
               Margin="{Binding ElementName=MainCombo, Path=Margin}" />
    </Grid>
</UserControl>

And all the magic is in code behind:
public static readonly DependencyProperty RelatedComboBoxProperty =
            DependencyProperty.Register("RelatedComboBox", typeof(ComboBox), 
            typeof(PhantomValueComboBoxUserControl), new PropertyMetadata(null, RelatedComboBoxChangedCallback));

private static void RelatedComboBoxChangedCallback(DependencyObject dependencyObject, 
                    DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
    var thisControl = dependencyObject as PhantomValueComboBoxUserControl;
    var relatedCombo = dependencyPropertyChangedEventArgs.NewValue as ComboBox;
    if (relatedCombo != null && thisControl != null)
    {
        thisControl.FontFamily = relatedCombo.FontFamily;
        thisControl.FontSize = relatedCombo.FontSize;
        thisControl.MainComboBox.Style = relatedCombo.Style;
        thisControl.Margin = new Thickness(0);
        thisControl.HorizontalAlignment = relatedCombo.HorizontalAlignment;
        thisControl.MainComboBox.HorizontalAlignment = relatedCombo.HorizontalAlignment;
        thisControl.MainComboBox.VerticalAlignment = relatedCombo.VerticalAlignment;
        thisControl.VerticalAlignment = relatedCombo.VerticalAlignment;
        relatedCombo.SelectionChanged += (sender, args) =>
        {
            thisControl._internalIndexChange = true;
            if (relatedCombo.SelectedIndex < thisControl.MainComboBox.Items.Count)
            {
                thisControl.MainComboBox.SelectedIndex = relatedCombo.SelectedIndex;
            }
            thisControl._internalIndexChange = false;
        };

        var mb = new MultiBinding
        {
            Converter = new MathToVisibilityMultiConverter(),                 
        };

        var mb1 = new Binding
        {
            Source = relatedCombo,
            Path = new PropertyPath("SelectedIndex")
        };

        var mb2 = new Binding
        {
            Source = thisControl,
            Path = new PropertyPath("SelectedOptionText")
        };

        mb.Bindings.Add(mb1);
        mb.Bindings.Add(mb2);
        thisControl.SetBinding(VisibilityProperty, mb);
        
        var b1 = new Binding
        {
            Source = relatedCombo,
            Path = new PropertyPath("IsEnabled")
        };
        thisControl.SetBinding(IsEnabledProperty, b1);

        var b2 = new Binding
        {
            Source = relatedCombo,
            Path = new PropertyPath("ItemsSource")
        };
        thisControl.SetBinding(ItemsSourceProperty, b2);
    }
}

Enjoy! 
Download the source code (Visual Studio 2013 project).

1 comment: