Binding RadioButtons to objects not working under Update Controls

Developer
Aug 3, 2012 at 1:20 AM
Edited Aug 3, 2012 at 1:22 AM

I developed a neat way to bind RadioButtons to almost anything, using this ValueConverter:

/// <summary>Converts an value to 'true' if it matches the 'To' property.</summary>
/// <example>
/// <RadioButton IsChecked="{Binding VersionString, Converter={local:TrueWhenEqualTo '1.0'}}"/>
/// </example>
public class TrueWhenEqualTo : MarkupExtension, IValueConverter
{
	public override object ProvideValue(IServiceProvider serviceProvider)
	{
		return this;
	}

	public TrueWhenEqualTo(object to) { To = to; }
	public object To { get; set; }

	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
	{
		return object.Equals(value, To);
	}
	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
	{
		if ((bool)value) return To;
		throw new NotSupportedException();
	}
}

For example, you can use this to bind RadioButtons to a string property as follows (it is a well-known bug in WPF that you must use a unique GroupName for each RadioButton):

<RadioButton GroupName="G1" Content="Cat"
    IsChecked="{Binding Animal, Converter={local:TrueWhenEqualTo 'CAT'}}"/>
<RadioButton GroupName="G2" Content="Dog"
    IsChecked="{Binding Animal, Converter={local:TrueWhenEqualTo 'DOG'}}"/>
<RadioButton GroupName="G3" Content="Horse"
    IsChecked="{Binding Animal, Converter={local:TrueWhenEqualTo 'HORSE'}}"/>

Without UpdateControls, this also works when you want to bind to static objects:

<RadioButton GroupName="F1" Content="Filter One" 
    IsChecked="{Binding Filter, Converter={local:TrueWhenEqualTo {x:Static local:FilterVM.Filter1}}}"/>
<RadioButton GroupName="F2" Content="Filter Two"
    IsChecked="{Binding Filter, Converter={local:TrueWhenEqualTo {x:Static local:FilterVM.Filter2}}}"/>

The working codebehind without Update Controls is:

// In the window constructor: DataContext = new VM();
class VM : NPCHelper
{
	FilterVM _filter = FilterVM.Filter1;
	public FilterVM Filter // never null
	{
		get { return _filter; }
		set { _filter = value; FirePropertyChanged("Filter"); }
	}
	string _animal;
	public string Animal
	{
		get { return _animal; }
		set { _animal = value; FirePropertyChanged("Animal"); }
	}
}
public class FilterVM
{
	public static readonly FilterVM Filter1 = new FilterVM();
	public static readonly FilterVM Filter2 = new FilterVM();
}
public class NPCHelper : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;
	public void FirePropertyChanged(string prop)
	{
		if (PropertyChanged != null)
			PropertyChanged(this, new PropertyChangedEventArgs(prop));
	}
}

However, this kind of object binding doesn't work if I switch to Update Controls (although string binding does). The first problem is that the object passed to the ValueConverter is ObjectInstance`1, but that is easily solved:

	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
	{
		if (value is UpdateControls.XAML.Wrapper.IObjectInstance)
			value = ((UpdateControls.XAML.Wrapper.IObjectInstance)value).WrappedObject;
		return object.Equals(value, To);
	}

The second problem, however, is a total mystery because I can't figure out how to step through the .NET framework to see what happens. I was hoping you could guess what the problem is, Micheal. When the user clicks one of the radio buttons, ConvertBack is called with the expected value of true, so ConvertBack returns To.

But then the property setter is called with value==null:

Independent<FilterVM> _filter = new Independent<FilterVM>(FilterVM.Filter1);
public FilterVM Filter {
	get { return _filter; }
	set { _filter.Value = value; } // value==null!!!!
}

I don't know how to figure out what code in Update Controls is involved, if any. I mean, there's no "breakpoint on everything" AFAIK. Any ideas?

Coordinator
Aug 3, 2012 at 4:31 PM

I think you are getting the null from the other radio button: the one that is clearing. If you put an if (value != null) around that assignment and let it skip to the next one, you should see the correct value set on the second time through.

It's really unfortunate that your value converter has to know about IObjectInstance. I can't see a way around that without using ViewModelBase instead of ForView.Wrap. Or just sticking with primitive types like an Enum, and letting the selection of the view model be dependent.

Developer
Aug 3, 2012 at 5:59 PM
Edited Aug 3, 2012 at 7:00 PM

Nope. ConvertBack is called only once and the property setter is called only once (whether I use UC or not). Any other ideas?

I don't want to use ViewModelBase or plain INPC, but let's hypothetically imagine that I am going to use ViewModelBase or INPC. Which class should be changed to fix the second problem: VM or FilterVM?

(Also I don't want to use a plain enum because a complex object is the most natural implementation, and in the future new filters may be defined at runtime, so an enum wouldn't work anymore.)

EDIT: The answer is FilterVM. VM can use UpdateControls normally, which is a relief in my case because it would much be harder to change (the equivalent of) VM than FilterVM in my real application. However, I hope this will only be a temporary workaround until we can find the bug in UC.

P.S. I'd really appreciate it if you'd write an article or other documentation about how UpdateControls.XAML works. Being built on low-level WPF stuff and reflection makes everything unintuitive and non-obvious. I hardly remember the changes I myself made. Maybe some sort of diagram is in order.

Coordinator
Aug 6, 2012 at 7:12 PM
Edited Aug 6, 2012 at 7:14 PM

That is a tricky one. Let me see if I can follow what's happening.

FilterVM is used directly in XAML, so it does not go through ForView.Wrap. As a result, the converter's To property is set to an unwrapped instance of FilterVM.

The VM instance, however, does go through ForView.Wrap. Since the Filter property's type (FilterVM) is not primitive and does not implement INPC, it is also wrapped.

And so the converter is comparing a wrapped FilterVM in the value parameter with an unwrapped one in the To property. Hence the change that you made to Convert to unwrap the value.

Furthermore, on check, the converter is returning an unwrapped FilterVM, which XAML cannot assign to the Filter property. As a result, it coerces it into a null.

So by implementing INPC on FilterVM, you turned off the wrapper for the Filter property.

An alternative that might work is to wrap the public static instances in FilterVM:

public class FilterVM
{
    public static readonly object Filter1 = ForView.Wrap(new FilterVM());
    public static readonly object Filter2 = ForView.Wrap(new FilterVM());
}

On doing this, you should be able to remove the unwrapping code from Convert, since it will be comparing a wrapped FilterVM with another wrapped FilterVM.

You're right. A good writeup of what UpdateControls.XAML is doing would help diagnose these issues.

Developer
Aug 6, 2012 at 9:21 PM
Edited Aug 6, 2012 at 9:23 PM

Hmmmm, so are you saying that when ConvertBack returns Filter1 or Filter2, that WPF itself changes it to null, and UpdateControls is not involved in the decision? I thought UC would be involved, since VM is wrapped by UC. But there was no easy way to tell, since I couldn't put a breakpoint on UpdateControls.XAML as a whole and I couldn't debug WPF either (I actually found a VS extension to help me do that, but it didn't work, and a way to supposedly debug the .NET framework, but it didn't work either). I will try your solution when I get back to work.

Developer
Aug 7, 2012 at 11:38 PM
Edited Aug 7, 2012 at 11:40 PM

Yes, your idea did work (it didn't work at first -- but it worked after removing the (value is IObjectInstance) check and changing "return _filter" to "return _filter.Value".)

However, it's too clumsy to use -- changing Filter1 and Filter2 to 'object' also meant I had to change VM.Filter to 'object'. Yuck.

Changing ConvertBack to the following also works:

	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
	{
		if ((bool)value) return new UpdateControls.XAML.Wrapper.ObjectInstance<FilterVM>(
			To, System.Windows.Threading.Dispatcher.CurrentDispatcher);
		throw new NotSupportedException();
	}

But of course that's no good, since it's no longer a general-purpose ValueConverter. But I guess some reflection magic could make it general-purpose once again.

Developer
Aug 8, 2012 at 12:55 AM
Edited Aug 8, 2012 at 12:58 AM

Wait a minute! I don't need reflection magic, I can just use ForView.Wrap() in ConvertBack. Here's a version of TrueWhenEqualTo that is compatible with Update Controls:

public class TrueWhenEqualTo : MarkupExtension, IValueConverter
{
	public override object ProvideValue(IServiceProvider serviceProvider)
	{
		return this;
	}

	public TrueWhenEqualTo(object to) { To = to; }

	public object To { get; set; }

	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
	{
		if (value is UpdateControls.XAML.Wrapper.IObjectInstance)
			value = ((UpdateControls.XAML.Wrapper.IObjectInstance)value).WrappedObject;
		return object.Equals(value, To);
	}
	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
	{
		if ((bool)value) {
			if (To != null && typeof(UpdateControls.XAML.Wrapper.IObjectInstance).IsAssignableFrom(targetType))
				return ForView.Wrap(To);
			else
				return To;
		}
		throw new NotSupportedException();
	}
}
Coordinator
Aug 9, 2012 at 3:38 AM

That ought to work. Still, it's too bad that the value converter needs to know about Update Controls.

I guess you could write a MarkupExtension that calls ForView.Wrap, and then pass its result to TrueWhenEqualTo. I don't know that I like that any better, though.

Or, the candidate filters could be properties of the VM instead of FilterVM. Then you can get them from the data context rather than using x:Static. UC would wrap them for you before handing them to the value converter, so it would end up comparing two wrapped objects.

Developer
Aug 9, 2012 at 5:30 AM
Edited Aug 9, 2012 at 5:34 AM

I'm satisfied with my solution: it's better for the ValueConverter (which is essentially part of the view layer) to "know" about UpdateControls.XAML rather than the viewmodel, although your last idea avoids "explicit" knowledge. And separating the knowledge of UC into a separate MarkupExtension would mean that my XAML has to do things differently for UC than non-UC, which isn't ideal either.