The one that got away [Simple workarounds for a visual problem when toggling a ContextMenu MenuItem's IsEnabled property directly]
A few days ago, Martin Naughton and Tiago Halm de Carvalho e Branco independently contacted me to report a problem they were having with the new ContextMenu control in the April '10 release of the Silverlight Toolkit. In both cases, they were toggling the IsEnabled property of a MenuItem directly and reported that the control's visuals weren't updating correctly. I was a little surprised at first because I knew I'd tested dynamic changes to the enabled state and I'd seen them work properly. But once I created a test project to investigate the report, I saw how the problem scenario was different.
The approach I focused my testing on (and which works correctly by all accounts) is the ICommand (Command/CommandParameter) scenario where the enabled state of the MenuItem
is controlled by the CanExecute method of the ICommand
implementation. In this scenario, the MenuItem
changes its own IsEnabled
state and updates its visuals explicitly, so everything is always in sync. But the code from the bug reports wasn't using ICommand
; it was manipulating the IsEnabled
property directly. The bug is that MenuItem
doesn't find out about those changes - the indirect reason being that it doesn't own the IsEnabled
property (which it inherits from Control). Because MenuItem
doesn't know about the change, it doesn't know to update its visual state.
Fortunately, there are some easy workarounds!
Workarounds
- Do nothing. I've already checked in a fix for this bug and it will be part of the next Silverlight Toolkit release. If the scenario doesn't matter to you before then, you don't need to worry about it. Otherwise, maybe you can...
- Patch the code, recompile the
System.Windows.Controls.Input.Toolkit
assembly, and use that in your project. I don't expect most people will want to take this approach, but if it suits you, then it's the next best thing to having a new Toolkit build. Here's the unified diff for the change toMenuItem.cs
:@@ -143,6 +143,7 @@ public MenuItem() { DefaultStyleKey = typeof(MenuItem); + IsEnabledChanged += new DependencyPropertyChangedEventHandler(HandleIsEnabledChanged); UpdateIsEnabled(); } @@ -301,6 +302,16 @@ } /// <summary> + /// Called when the IsEnabled property changes. + /// </summary> + /// <param name="sender">Source of the event.</param> + /// <param name="e">Event arguments.</param> + private void HandleIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + ChangeVisualState(true); + } + + /// <summary> /// Changes to the correct visual state(s) for the control. /// </summary> /// <param name="useTransitions">True to use transitions; otherwise false.</param>
But if you're not sure how or where to apply that change (or what to do afterward), this is probably not the best option for you. Instead, you might... - Switch from manually toggling the
IsEnabled
property to doing so indirectly via anICommand
implementation. In general,ICommand
-based approaches are more consistent with the MVVM pattern and can be more architecturally pure. If you're not already familiar with the technique, it could be worthwhile to read about it: here's a overview of commanding in WPF. That said, it's not always convenient to make changes like this, and sometimes directly togglingIsEnabled
really is the best approach. If so, then another option is to... - "Bounce" the Template property to null and back after changing the IsEnabled property. The bug is mainly cosmetic: the internal state is correct, but the visuals aren't. Therefore, any change to
MenuItem
that prompts it to update its visual state will correct the problem. While giving theMenuItem
focus would work, too, a less intrusive way is to change the value of the control's Template property. But because we don't really want to change theTemplate
, it's necessary to restore the original value. The following code demonstrates this technique:// "Bouncing" the Template after toggling works around the issue menuItemBounce.IsEnabled = !menuItemBounce.IsEnabled; var template = menuItemBounce.Template; menuItemBounce.Template = null; menuItemBounce.Template = template;
Because a well-behaved control updates its visual states after getting a newTemplate
, and becauseMenuItem
is well-behaved (usually!), this "bounce" is enough to solve the problem. But maybe you're setting theIsEnabled
property with a Binding or don't want to incur the cost of swapping out visuals like this. No problem, you can always... - Set the
MenuItemIsEnabledWorkaround.IsActive
attached property (from the code in my sample project) for a seamless workaround. Based on the observation that direct manipulation of theIsEnabled
property is rarely associated with the use of anICommand
implementation and the fact that theICommand
scenario works properly today, I created a self-contained workaround that's easy to use. TheMenuItemIsEnabledWorkaround
class exposes an attached DependencyProperty and implements theICommand
interface. WhenIsActive
is set toTrue
on aMenuItem
, an instance of theMenuItemIsEnabledWorkaround
class is created and assigned to theMenuItem
'sCommand
property. This instance is also hooked up to theMenuItem
's IsEnabledChanged event - when that event fires, theMenuItemIsEnabledWorkaround
'sCanExecuteChanged
event is also fired and itsCanExecute
method reports the new value of theIsEnabled
property. That may sound complicated, but it's simple in practice:<toolkit:MenuItem x:Name="menuItemWorkaround" Header="MenuItem with workaround active" delay:MenuItemIsEnabledWorkaround.IsActive="True"/>
// Activating the workaround in XAML requires no code changes menuItemWorkaround.IsEnabled = !menuItemWorkaround.IsEnabled;
By changing theIsEnabled
scenario into anICommand
scenario,MenuItemIsEnabledWorkaround
sidesteps the bug and saves the day!
Examples
I've created a sample application to demonstrate the use of the last two workarounds in practice. It contains a simple ContextMenu
with three MenuItem
s and toggles their IsEnabled
state every second (whether the menu is open or not). You'll see either of the last two workarounds is enough to keep the corresponding MenuItem
's visual state up to date.
[Click here to download the MenuItemIsEnabledWorkaround sample application and source code.]
It's never fun when a bug sneaks by you.