0

I am new to WPF. I have a ComboBox and TextBox both should be bind to a single property based on the selection in combobox. If I select 'other', property should be bind to textbox else to combobox. Someone can please guide me.

I am really sorry for not being very clear. I have an object with property "Name". I am very new to WPF and this is my first work with WPF. I am really not sure this is the right way or not. Please help me.

Xaml file:

<mui:ModernWindow x:Class="TestOtherInBinding.MainWindow"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mui="http://firstfloorsoftware.com/ModernUI"
              Title="mui" 
              Style="{StaticResource BlankWindow}" Loaded="ModernWindow_Loaded">
<ScrollViewer>
    <StackPanel Name="spTest">
        <ComboBox Name="cmbTest" Width="140" Margin="5" SelectionChanged="cmbTest_SelectionChanged" SelectedValuePath="Content" >
            <ComboBoxItem Content="Name1"/>
            <ComboBoxItem Content="Name2"/>
            <ComboBoxItem Content="Name3"/>
            <ComboBoxItem Content="Name4"/>
            <ComboBoxItem Content="Other"/>
        </ComboBox>
        <TextBox Name="txtTest"  Width="140" Margin="5">
        </TextBox>
        <Button Content="Submit" Width="80" />
    </StackPanel>

</ScrollViewer>

Code Behind:

public partial class MainWindow : ModernWindow
{
    public MainWindow()
    {
        InitializeComponent();

    }
    public string MyProperty { get; set; }

    Binding bin = new Binding("Name");

    private void cmbTest_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        bin.ValidationRules.Add(new ExceptionValidationRule());

        if (cmbTest.SelectedValue.ToString() == "Other")
        {
            txtTest.Visibility = Visibility.Visible;
            BindingOperations.ClearBinding(cmbTest, ComboBox.TextProperty);
            BindingOperations.SetBinding(txtTest, TextBox.TextProperty, bin);
        }
        else
        {
            txtTest.Visibility = Visibility.Collapsed;
            BindingOperations.ClearBinding(txtTest, TextBox.TextProperty);
            BindingOperations.SetBinding(cmbTest, ComboBox.TextProperty, bin);
        }
    }

    private void ModernWindow_Loaded(object sender, RoutedEventArgs e)
    {
        Peron p = new Peron();
        spTest.DataContext = p;
        txtTest.Visibility = Visibility.Collapsed;
        BindingOperations.SetBinding(cmbTest, ComboBox.TextProperty, bin);

        if (p.Name != string.Empty)
            cmbTest.SelectedIndex = 0;
    }
}

Object:

class Peron
{
    string name;
    public string Name
    {
        get
        {  return name;  }
        set
        {
            if (value == string.Empty)
            {
                throw new Exception("Name Should not be empty");
            }
        }
    }

When i changed the selection to "Other", clearbinding on combobox is throwing exception.

Thanks, Ram

Ugur
  • 1,257
  • 2
  • 20
  • 30
KV Ramana
  • 89
  • 3
  • 11
  • 1
    To what property do you want the `ComboBox` and `TextBox` bound? Some other XAML element? Some code-behind object? What does the code-behind look like? Would it be appropriate to simply bind each of the `ComboBox` and `TextBox` to two separate properties and resolve the "other" aspect entirely in code-behind rather than via additional binding logic? – Peter Duniho Feb 15 '15 at 09:00
  • Hi Peter, thanks for the response. I am sorry not being clear. I added entire code. Kindly review and help me. – KV Ramana Feb 15 '15 at 12:26

3 Answers3

2

I'm not exactly sure how you can improve this, but the exception is clearly related to this line: BindingOperations.SetBinding(txtTest, TextBox.TextProperty, bin);

When the Text of ComboBox is changed, the SelectedValue is set to null, and the cmbTest_SelectionChanged is called again and the exception is thrown.

Suggestion:

  1. You may want to modify the ControlTemplate of ComboBox creating a Custom ComboBox that supports some kind of overlay text.
  2. Don't manually change the text of combo box, instead use anotherText:

    <Grid> <ComboBox .../> <Border Background="White"> <TextBlock x:Name="anotherText"/> </Border> </Grid>

.


Previous Answer:

You can use the PropertyChangedCallback of each DependencyProperty (DP) in your ViewModel in order do modify the ViewModel properties and check for advanced conditions.

First create a ViewModel (empty class named MainVm). Then set the DataContext of MainWindow to an instance of it.

public MainWindow()
{
    InitializeComponent();
    DataContext = new MainVm();
}

MainVm has four key DPs:

  1. SelectedItem (bound to ComboBox.SelectedItem)
  2. IsLastItemSelected (bound to Last ComboBoxItem's IsSelected)
  3. Text (bound to TextBox.Text)
  4. Result (output)

so that the MainVm should look like this:

public class MainVm : DependencyObject
{

    /// <summary>
    /// Gets or sets a bindable value that indicates ComboBox SelectedItem
    /// </summary>
    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(MainVm),
        new PropertyMetadata(null, (d, e) =>
        {
            //property changed callback
            var vm = (MainVm)d;
            var val = (object)e.NewValue;
            if(val!=null && !vm.IsLastItemSelected )
                //Result =  SelectedItem,   if the last item is not selected
                vm.Result = val.ToString();
        }));


    /// <summary>
    /// Gets or sets a bindable value that indicates custom Text
    /// </summary>
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }
    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(MainVm),
        new PropertyMetadata("", (d, e) =>
        {
            //property changed callback
            var vm = (MainVm)d;
            var val = (string)e.NewValue;
            //Result =  Text,           if last item is selected
            //          SelectedItem,   otherwise
            vm.Result = vm.IsLastItemSelected ? val : vm.SelectedItem.ToString();
        }));


    /// <summary>
    /// Gets or sets a bindable value that indicates whether the 
    ///   LastItem of ComboBox is Selected
    /// </summary>
    public bool IsLastItemSelected
    {
        get { return (bool)GetValue(IsLastItemSelectedProperty); }
        set { SetValue(IsLastItemSelectedProperty, value); }
    }
    public static readonly DependencyProperty IsLastItemSelectedProperty =
        DependencyProperty.Register("IsLastItemSelected", typeof(bool), typeof(MainVm),
        new PropertyMetadata(false, (d, e) =>
        {
            //property changed callback
            var vm = (MainVm)d;
            var val = (bool)e.NewValue;
            //Result =  Text,           if last item is selected
            //          SelectedItem,   otherwise
            vm.Result = val ? vm.Text : vm.SelectedItem.ToString();
        }));


    /// <summary>
    /// Gets or sets a bindable value that indicates Result
    /// </summary>
    public string Result
    {
        get { return (string)GetValue(ResultProperty); }
        set { SetValue(ResultProperty, value); }
    }
    public static readonly DependencyProperty ResultProperty =
        DependencyProperty.Register("Result", typeof(string),
        typeof(MainVm), new PropertyMetadata("select something..."));
}

Now you can bind these DPs to your View:

<StackPanel>
    <ComboBox Name="cmbTest" Width="140" Margin="5" SelectedItem="{Binding SelectedItem}">
        <ComboBoxItem Content="Name1"/>
        <ComboBoxItem Content="Name2"/>
        <ComboBoxItem Content="Name3"/>
        <ComboBoxItem Content="Name4"/>
        <ComboBoxItem Content="Other" IsSelected="{Binding IsLastItemSelected}"/>
    </ComboBox>
    <TextBox Width="140" Margin="5" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"/>
    <TextBlock Text="{Binding Result}"/>
</StackPanel>
Bizhan
  • 16,157
  • 9
  • 63
  • 101
  • Hi Bizz, thanks for the reply. I reviewed your answer. I don't know MVVM as i am very beginner for WPF. My understanding of this code is not so good. – KV Ramana Feb 15 '15 at 12:32
  • 1
    I modified my answer. Please read the first part. If you are interested I suggest start learning WPF with MVVM pattern. It's the most efficient way. http://stackoverflow.com/a/2034333/366064 – Bizhan Feb 15 '15 at 16:29
1

The exception you are getting, due to the cmbTest.SelectedValue property being null, happens because when the user selects "Other" in the ComboBox, you execute this statement:

BindingOperations.ClearBinding(cmbTest, ComboBox.TextProperty);

I.e. when you clear the binding to the ComboBox's Text property, the binding system resets the property value to null. This causes your event handler to be called recursively, but this time the SelectedValue property is null, and dereferencing causes the NullReferenceException.

You could fix this by checking the property value for null before trying to dereference it. But while this would avoid the exception, it won't change the fact that changing the bindings dynamically causes changes in the various bound values, in a variety of undesirable ways.

Instead of trying to get your current approach to work, it's my opinion you should try to work more closely with the binding system itself to produce the desired results.


One way to do this would be to follow the suggestion in my comment:

"Would it be appropriate to simply bind each of the ComboBox and TextBox to two separate properties and resolve the "other" aspect entirely in code-behind rather than via additional binding logic?"

And in fact, this is essentially the approach provided in the answer from Bizz. IMHO his answer is useful and worth taking a look at. That said, he implemented it somewhat differently from how I would have, so I will share my version of that particular approach:

XAML:

<Window x:Class="TestSO28524422ComboAndTextToProperty.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TestSO28524422ComboAndTextToProperty"
        Title="MainWindow" Height="350" Width="525">
  <StackPanel>
    <StackPanel.DataContext>
      <local:Person/>
    </StackPanel.DataContext>
    <ComboBox Name="cmbTest" Width="140" Margin="5" SelectedValuePath="Content"
              SelectedValue="{Binding ComboBoxText}">
      <ComboBoxItem Content="Name1"/>
      <ComboBoxItem Content="Name2"/>
      <ComboBoxItem Content="Name3"/>
      <ComboBoxItem Content="Name4"/>
      <ComboBoxItem Content="Other" IsSelected="{Binding IsOtherSelected}"/>
    </ComboBox>
    <TextBox Name="txtTest"  Width="140" Margin="5"
             Text="{Binding TextBoxText, UpdateSourceTrigger=PropertyChanged}">
      <TextBox.Style>
        <Style TargetType="TextBox">
          <Style.Triggers>
            <DataTrigger Binding="{Binding IsOtherSelected}" Value="False">
              <Setter Property="Visibility" Value="Collapsed"/>
            </DataTrigger>
          </Style.Triggers>
        </Style>
      </TextBox.Style>
    </TextBox>
    <TextBlock Text="{Binding Name}"/>
  </StackPanel>
</Window>

C#:

public class Person : DependencyObject
{
    public static readonly DependencyProperty IsOtherSelectedProperty =
        DependencyProperty.Register("IsOtherSelected", typeof(bool), typeof(Person));
    public static readonly DependencyProperty ComboBoxTextProperty =
        DependencyProperty.Register("ComboBoxText", typeof(string), typeof(Person),
        new PropertyMetadata(OnComboBoxTextChanged));
    public static readonly DependencyProperty TextBoxTextProperty =
        DependencyProperty.Register("TextBoxText", typeof(string), typeof(Person),
        new PropertyMetadata(OnTextBoxTextChanged));
    public static readonly DependencyProperty NameProperty =
        DependencyProperty.Register("Name", typeof(string), typeof(Person));

    public bool IsOtherSelected
    {
        get { return (bool)GetValue(IsOtherSelectedProperty); }
        set { SetValue(IsOtherSelectedProperty, value); }
    }
    public string ComboBoxText
    {
        get { return (string)GetValue(ComboBoxTextProperty); }
        set { SetValue(ComboBoxTextProperty, value); }
    }

    public string TextBoxText
    {
        get { return (string)GetValue(TextBoxTextProperty); }
        set { SetValue(TextBoxTextProperty, value); }
    }

    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        private set { SetValue(NameProperty, value); }
    }

    private static void OnComboBoxTextChanged(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Person person = (Person)d;
        string value = (string)e.NewValue;

            person.Name = person.IsOtherSelected ? person.TextBoxText : value;
    }

    private static void OnTextBoxTextChanged(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Person person = (Person)d;
        string value = (string)e.NewValue;

            person.Name = person.IsOtherSelected ? value : person.ComboBoxText;
    }
}

Notes:

  • As in Bizz's answer, I've bound the derived property (in this case, Name) to a TextBlock just so it's easy to see updates in the UI. You may or may not need to do something like this in your real-world code.
  • The foundation for this approach is a DependencyObject subclass that serves as the actual target for the binding (i.e. the DataContext value). You can either set your program up so that this is in fact the data object for your code-behind logic, or (as in the MVVM pattern) a go-between that handles binding tasks for your actual data object. The important thing is that it's a DependencyObject so that it can be used directly in the binding syntax in XAML (implementing INotifyPropertyChanged should work as well).
  • Instead of setting the DataContext in the code-behind, it is set here in the XAML. IMHO, if something can be handled in the XAML instead of the code-behind it is better to do so. This allows the XAML editor and compiler to know more about the implementation details and provide better feedback, in the form of Intellisense or a compile-time error as appropriate.
  • The part that takes care of mapping the appropriate UI selection to your Name property are the PropertyChangedCallback methods, OnComboBoxTextChanged() and OnTextBoxTextChanged(). When the DependencyProperty values for these properties are registered, these callback methods are provided for the properties and are called any time the values of the properties change. Each updates the Name property according to the current value of the IsOtherSelected property (which itself is automatically kept up-to-date by the binding system, it having been bound to the IsSelected property of the last item in your ComboBox).
  • Note that there's not actually any need to respond to changes in the IsOtherSelected property. We know that property will change only if the SelectedValue property changes, and changes to that property will already update the Name property value.
  • One of the things your original code was doing was setting the Visibility of the TextBox to Collapsed unless the "Other" value was selected in the ComboBox. This is effected here by setting up a DataTrigger in the Style for the TextBox. The default Visibility is Visible, and it stays that way as long as the trigger isn't triggered. But if it is triggered, i.e. the IsOtherSelected property becomes false, the Setter in the DataTrigger will be applied, setting the Visibility value to Collapsed as desired. If and when the trigger is no longer satisfied, WPF sets the property value back to its previous value (i.e. Visible).

IMHO it is worth taking the time to understand this approach. I understand (having been through this recently and in fact continuing to go through it myself) that learning the XAML/binding-based philosophy of WPF is daunting. There are often many different ways to do things in WPF and it's not always clear which is the best way. At the same time, it is harder to detect and understand errors in the XAML (hint: check the run-time output for the program in the debugger's "Output" window). But the binding system can handle a lot of the presentation scenarios that come up in a GUI-based program, and in many cases all of the scenarios in a given program. Working with the API is in the long run a lot easier, even if much harder at the outset. :)


Finally, I'll share a second way to accomplish the same thing. Instead of handling the mapping in your DependencyObject subclass itself (i.e. through separate bound properties in the subclass which are mapped to a third property), you can create a MultiBinding object with a custom converter that knows how to integrate two or more input values to produce a single output value.

Unfortunately, I wasn't able to figure out how to configure this in the XAML (did I mention I'm still learning it myself? :) ), but the code-behind is not complicated. If there is a way to get it set up in the XAML, it will look very similar: a MultiBinding object goes in place of a Binding, the Converter property is set for it, and the multiple Binding objects are added to the MultiBinding object as children. This in fact does often work normally; I think there's just something about trying to bind to DataContext object rather than a FrameworkElement subclass that I haven't figured out yet.

Anyway, the code looks like this:

XAML:

<Window x:Class="MultiBindingVersion.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MultiBindingVersion"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <local:ComboAndTextToTextConverter x:Key="comboAndTextToTextConverter1"/>
  </Window.Resources>
  <StackPanel x:Name="spTest">
    <StackPanel.DataContext>
      <local:Person/>
    </StackPanel.DataContext>
    <ComboBox x:Name="cmbTest" Width="140" Margin="5" SelectedValuePath="Content">
      <ComboBoxItem Content="Name1"/>
      <ComboBoxItem Content="Name2"/>
      <ComboBoxItem Content="Name3"/>
      <ComboBoxItem Content="Name4"/>
      <ComboBoxItem x:Name="otherItem" Content="Other"/>
    </ComboBox>
    <TextBox x:Name="txtTest"  Width="140" Margin="5">
      <TextBox.Style>
        <Style TargetType="TextBox">
          <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=otherItem, Path=IsSelected}"
                         Value="False">
              <Setter Property="Visibility" Value="Collapsed"/>
            </DataTrigger>
          </Style.Triggers>
        </Style>
      </TextBox.Style>
    </TextBox>
    <TextBlock x:Name="textBlock1" Text="{Binding Path=Name}"/>
  </StackPanel>
</Window>

C#:

public class Person : DependencyObject
{
    public static readonly DependencyProperty NameProperty =
        DependencyProperty.Register("Name", typeof(string), typeof(Person));

    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        set { SetValue(NameProperty, value); }
    }
}

class ComboAndTextToTextConverter : IMultiValueConverter
{
    public object Convert(object[] values,
        Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string comboBoxText = values[1] as string;
        string textBoxText = values[2] as string;

        if (values[0] is bool && comboBoxText != null && textBoxText != null)
        {
            bool otherItemIsSelected = (bool)values[0];
            return otherItemIsSelected ? textBoxText : comboBoxText;
        }

        return Binding.DoNothing;
    }

    public object[] ConvertBack(object value,
        Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        _SetMultibinding((Person)spTest.DataContext, Person.NameProperty,
            Tuple.Create((DependencyObject)otherItem, ComboBoxItem.IsSelectedProperty),
            Tuple.Create((DependencyObject)cmbTest, ComboBox.SelectedValueProperty),
            Tuple.Create((DependencyObject)txtTest, TextBox.TextProperty));
    }

    private void _SetMultibinding(DependencyObject target,
        DependencyProperty property,
        params Tuple<DependencyObject, DependencyProperty>[] properties)
    {
        MultiBinding multiBinding = new MultiBinding();

        multiBinding.Converter =
            (IMultiValueConverter)Resources["comboAndTextToTextConverter1"];

        foreach (var sourceProperty in properties)
        {
            Binding bindingT = new Binding();

            bindingT.Source = sourceProperty.Item1;
            bindingT.Path = new PropertyPath(sourceProperty.Item2);
            multiBinding.Bindings.Add(bindingT);
        }

        BindingOperations.SetBinding(target, property, multiBinding);
    }
}

Note:

  • In this case, the Person object has only the Name property needed, with just the basic DependencyProperty implementation. In the other example, the Person object exists as the source for the bound properties (even though logically it's the target). But with the MultiBinding, the target really does need to be the target. Since the binding will be specified in code-behind, the XAML doesn't have any bindings declared (except the visibility trigger), and the Person object thus doesn't need those individual properties to which the XAML elements can be bound.
  • There's a new class here, ComboAndTextToTextConverter. This is the class that will accept the multiple inputs and convert them to a single output. You can see reading the code that it takes three values as input: a bool, and two string values (the ComboBox.SelectedValue and TextBox.Text property values, respectively). The code does some minimal validation on the types and then maps the values as appropriate.
  • The window's code-behind is where the MultiBinding is created and set. I have a simple helper method that takes the target object and property, and a variable number of source objects and properties. It simply loops through the sources, adding them to the MultiBinding object, and then setting that MultiBinding object on the target object's property.
  • Since the Person object no longer has the helper properties, including the IsOtherSelected property, I went ahead and named the last ComboBox item so that it can be bound by name in the trigger for the TextBox's Visibility property.
  • I used the converter from the window's resources, but you could as easily not bother putting the converter into the resources and just create a new instance, i.e. new ComboAndTextToTextConverter(). Mostly my choice here is an artifact of my earlier attempts to get the MultiBinding configured via XAML (where the converter would need to be declared as a resource).


Sorry for the lengthy answer. I wanted to try to explain everything with reasonable detail, i.e. the sort of detail I wished I'd had when I was trying to figure this stuff out myself. :) I hope it helps!

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
0

If I understand you correctly you want to bind a textbox text to the value of the item selected in combobox, and if the combobox item "Other" selected, the text in the textbox is set to empty. One way to achieve that like so.

        <StackPanel Name="spTest" >
        <ComboBox Name="cmbTest" Width="140" Margin="5" SelectedValuePath="Tag" >
            <ComboBoxItem Content="Name1" Tag="Name 1" IsSelected="True" />
            <ComboBoxItem Content="Name2" Tag="Name 2"/>
            <ComboBoxItem Content="Name3" Tag="Name 3"/>
            <ComboBoxItem Content="Name4" Tag="Name 3"/>
            <ComboBoxItem Content="Other" Tag=""/>
        </ComboBox>
        <TextBox Name="txtTest" Text="{Binding ElementName=cmbTest, Path=SelectedValue}"  Width="140" Margin="5" >
        </TextBox>
        <Button Content="Submit" Width="80" />
    </StackPanel>

or if you want to have your textbox enabled only if "Other" is selected in your combobox, you can do...

        <StackPanel Name="spTest">
        <ComboBox Name="cmbTest" Width="140" Margin="5" SelectedValuePath="Tag"  >
            <ComboBoxItem Content="Name1" Tag="Name 1" IsSelected="True" />
            <ComboBoxItem Content="Name2" Tag="Name 2"/>
            <ComboBoxItem Content="Name3" Tag="Name 3"/>
            <ComboBoxItem Content="Name4" Tag="Name 3"/>
            <ComboBoxItem Content="Other" />
        </ComboBox>
        <TextBox Name="txtTest" Text="{Binding ElementName=cmbTest, Path=SelectedValue}"  Width="140" Margin="5" >
            <TextBox.Style>
                <Style>
                    <Setter Property="TextBox.IsEnabled" Value="False" />
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ElementName=cmbTest,Path=SelectedValue, Mode=OneWay}" Value="{x:Null}">
                            <Setter Property="TextBox.IsEnabled" Value="True" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>
        <Button Content="Submit" Width="80" />
    </StackPanel>
Aegir
  • 899
  • 8
  • 19
  • Hi Aegir, thanks for the response. I am not looking for element binding. I edited with full details. Can you please look into it. – KV Ramana Feb 15 '15 at 12:27
  • 1
    your code will work if you null check cmbText.SelectedValue: if (cmbTest.SelectedValue != null && cmbTest.SelectedValue.ToString() == "Other") You are getting a second run on cmbTest_SelectionChanged event because you are calling: BindingOperations.SetBinding(cmbTest, ComboBox.TextProperty, bin); and the second time the SelctedItem is null. – Aegir Feb 15 '15 at 12:53