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.Toolkitassembly, 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
IsEnabledproperty to doing so indirectly via anICommandimplementation. 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 togglingIsEnabledreally 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
MenuItemthat prompts it to update its visual state will correct the problem. While giving theMenuItemfocus 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 becauseMenuItemis well-behaved (usually!), this "bounce" is enough to solve the problem. But maybe you're setting theIsEnabledproperty 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.IsActiveattached property (from the code in my sample project) for a seamless workaround. Based on the observation that direct manipulation of theIsEnabledproperty is rarely associated with the use of anICommandimplementation and the fact that theICommandscenario works properly today, I created a self-contained workaround that's easy to use. TheMenuItemIsEnabledWorkaroundclass exposes an attached DependencyProperty and implements theICommandinterface. WhenIsActiveis set toTrueon aMenuItem, an instance of theMenuItemIsEnabledWorkaroundclass is created and assigned to theMenuItem'sCommandproperty. This instance is also hooked up to theMenuItem's IsEnabledChanged event - when that event fires, theMenuItemIsEnabledWorkaround'sCanExecuteChangedevent is also fired and itsCanExecutemethod reports the new value of theIsEnabledproperty. 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 theIsEnabledscenario into anICommandscenario,MenuItemIsEnabledWorkaroundsidesteps 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 MenuItems 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.