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!