Why ContextMenu.MenuItem don't look like they are disabled when attached action can't be performed?

May 14, 2012 at 3:37 AM
Edited May 14, 2012 at 3:30 PM

Can anybody tells me what is wrong with the following code?

View XAML:

 

 <Grid x:Name="LayoutRoot" Background="White">
            <ListBox Name="Items">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Name}">
                            <toolkit:ContextMenuService.ContextMenu>
                                <toolkit:ContextMenu>
                                    <toolkit:MenuItem Header="Remove" cal:Action.TargetWithoutContext="{Binding ElementName=LayoutRoot, Path=DataContext}" cal:Message.Attach="RemoveItem($dataContext)"/>
                                </toolkit:ContextMenu>
                            </toolkit:ContextMenuService.ContextMenu>
                        </TextBlock>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>       
    </Grid>

 

ViewModel Code:

 

 [Export(typeof(IScreen))]
    public class Page1ViewModel : Screen
    {
        public ObservableCollection<Item> Items { get; private set; }

        protected override void OnInitialize()
        {
            base.OnInitialize();
            Items = new ObservableCollection<Item>();
            for(int i =0; i < 5; i++)
                Items.Add(new Item{Name="Test" + i});
        }

        public bool CanRemoveItem(Item i)
        {
            return i.Name == "Test1";
        }

        public void RemoveItem(Item i)
        {
            Items.Remove(i);
        }
    }

    public class Item
    {
        public string Name { get; set; }
    }

Also need to add the following code in the AppBootstrapper configure function:

  

       protected override void Configure()
        {
            ConventionManager.AddElementConvention<MenuItem>(MenuItem.HeaderProperty, "DataContext", "Click");
           
             ....//Rest of the configure function code

         }

The code works partially. The RemoveItem function is only called when CanRemoveItem function returns true which is an expected behavior.  The problem is that the Items are not grayed out when they can't be removed.

I guess my question should be how to bind MenuItem.IsEnabled property to the CanRemoveItem function?

When I bind MenuItem.Command to a ICommand, everything works as expected. But I'm trying to use Caliburn Actions to replace all the Commands.

 

 

May 14, 2012 at 8:23 AM

Have you checked when and how many times the CanRemoveItem method is called, and what is the item value?

I tend to use properties to implement action guards, trying to avoid parametrized methods, to have full control over guard re-evaluation (through property change notifications).

Note that ICommand CanExecute methods are re-evaluated periodically, while action guards are 'one-shot', unless you use properties and raise property change notification when needed.

May 14, 2012 at 1:53 PM
BladeWise wrote:

Have you checked when and how many times the CanRemoveItem method is called, and what is the item value?

I tend to use properties to implement action guards, trying to avoid parametrized methods, to have full control over guard re-evaluation (through property change notifications).

Note that ICommand CanExecute methods are re-evaluated periodically, while action guards are 'one-shot', unless you use properties and raise property change notification when needed.

When I put a break point at CanRemoveItem function, I saw it get called twice (I don't know why) when I right click a item to open the context menu for the item. Both with the same item value so the function returned the same boolean value.

As I said, the function returns correct value. If CanRemoveItem returns false, RemoveItem function is not fired which is correct. The only problem is that the MenuItem still looks like it is enabled while it should be grayed out.  

I'm not sure I understand what you mean by using properties implement action guards in this situation. Could you show some code?  Thanks!

May 14, 2012 at 2:19 PM

Instead of implementing the guard as a function, create a CanRemoveItem property, and use the

NotifyPropertyChanged(()=>CanRemoveItem);

to notify the system about the change in the property value.

The only issue is that your guard requires a parameter (i.e. the item to be removed), so you probably need to re-design that; consider providing a SelectedItem property, raise the property change notification every time the selected item changes, and modify the CanRemove implementation to use the SelectedItem instead of the parameter currently passed to the method, or alternatively, move the CanRemoveItem/RemoveItem pair into the view-model associated to the MenuItem.

That said, I suppose that the guard is evaluated:

  1. when the context menu item is created
  2. when the context menu item is loaded

hence the two times. If the menu item is never disabled, there is a high chance that the enabling state does not reflect the current guard value.

You can find more info about action guard properties here.

May 14, 2012 at 3:27 PM

Actually, with my code, I found MenuItem.IsEnabledChanged event is fired and MenuItem.IsEnabled is set correctly after CanRevomeItem Guard function returns. It is just the Text does not turn to gray. 

If I set MenuItem.IsEnabled = false in the XAML like this:

<toolkit:MenuItem Header="Remove" IsEnabled="False" cal:Action.TargetWithoutContext="{Binding ElementName=LayoutRoot, Path=DataContext}" cal:Message.Attach="RemoveItem($dataContext"/>

The disabled Items now are working correctly. But the items should be enabled are initially grayed out, until I mouse over the MenuItem. This is a very strange behavior.