Friday, April 22, 2011

WPF: Trigger overwrites Setter (duh!)

As usual when I stumble into a problem that I can't solve only to see that it has a simple explanation and that I am not sufficiently informed on the matter, I had some misgivings on blogging about it. But these are the best possible blog entries, the "Oh, I am so stupid!" moments, because other people are sure to make the same mistake and you wouldn't wish for them the same humiliation, would you? Well, I wouldn't :-P

So here it is: I was having a ToggleButton and a ContextMenu in a custom control. I wanted to have the IsChecked property bound in two-way mode with the IsOpen property of the ContextMenu. So I did what most people would do, I created a Setter on the IsChecked property with a Binding on DropDown.IsOpen as a value (DropDown is the property of the control holding the ContextMenu). And it worked, of course. My custom control would inherit from ToggleButton and use the style with the Setter in it.

But now comes the tricky part: I wanted that when a certain condition was met, the button be checked and the menu open. So I added a Trigger to the Style on the condition that I wanted, with a Setter on the IsChecked property to True. And from then on, nothing made any sense. I would click the button and it would not open the menu anymore.

Well, if you think about it, you have a Setter in the Style and then another Setter on the same property in the Trigger. It makes a sort of a sense for them to interfere with each other, but I also know that setting a Binding as value to a DependencyProperty is like using SetBinding on the owner of the property. And I thought it was normal for the IsChecked property to be set to true from the trigger and, in turn, change the value of IsOpen. But it didn't happen. Let it be a lesson to other bozos like myself that this thing does make sense logically (or as wishful thinking), but not in WPF. And here is why:

Let's try to evaluate the values of IsChecked and IsOpen. First case scenario: clicking on the button. That changes the status of IsChecked from true to false and viceversa. Internally, what does happen is WPF finds any bindings associated with the value and refreshes them. In this case, it finds a binding to IsOpen and so the ContextMenu also opens up. Second case scenario: the condition in the ViewModel is met, the trigger is fired and the value of IsChecked is set. It should do the same thing, right? Find the bindings and refresh them. And it does! But in that fateful moment, it sees that the IsChecked property has a setter associated with it, from the trigger in the style, that sets it to True. It does not go further, because the trigger setter overrides the style setter. I personally think this is closer to a bug than a reasonable behavior, but I am biased here :) I mean, you set the value of the property directly in the code IsChecked=true;, and it works, but you set it in the trigger and it overrides the binding?

There are more solutions to this problem. One solution would be to replace the setter value in the trigger with another binding also to the IsOpen property of the ContextMenu, but with a nifty converter. I haven't tried it, because I think that, if it worked, it would add too much weird complexity and even if I love weird, this is a little bit too much for me. Of course, a programmatic solution is also possible, adding handlers for the Open and Close events of the ContextMenu and setting IsChecked, as well as setting IsOpen based on the value sent to the IsChecked property change callback. But I wanted to do something that is as WPFish as possible. Another solution is to set a binding to the IsOpen property, since it is two way, and this is what I did. Unfortunately, the ContextMenu is a variable of the control, so I had to manually set the binding in that property's PropertyChangedCallback. A more elegant solution, I believe, is to have another element present that would have two-way bindings for both IsChecked and IsOpen properties to two of its own properties. Internally, when one changes, the other is synchronized. This leaves both ToggleButton and ContextMenu free to have any setters on IsChecked and IsOpen without interference.

The pattern of using a separate control to link properties from other controls together is called Binding Hub. Here is an article detailing it. I disagree with the naming of properties as I believe for each connection a separate hub should be created with properties that actually make sense :), but the concept is powerful and I like it.