Sean Hall
Sean Hall

Reputation: 7878

Hide certain XAML defined context menu items unless shift was pressed

I have a DataTemplate that defines the context menu:

<DataTemplate>
    <TextBlock>
        <TextBlock.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding SendToRecycleBin}" Header="Delete">
            </ContextMenu>
        </TextBlock.ContextMenu>
    </TextBlock>
</DataTemplate>

I want to add another menu item to the context menu that is only shown if the user held down Shift while opening the Context Menu, using only XAML (maybe create a new attached property App.PowerUserOnly?):

<MenuItem Command="{Binding Delete}" Header="Permanently Delete"
                                     local:App.PowerUserOnly="true">

Can this be done in XAML only (if so, how?), or do you have to use code behind?

Edit: The Windows shell also shows advanced options when Shift was held down while opening the context menu. I'm trying to emulate that behavior. For instance, one of the advanced options for an application is to run it as a different user.

I simplified my code to help test out people's suggestions. The project is created in VS2010 with the default WPF Application, named ShiftContextMenu. The App.xaml and App.xaml.cs files are unmodified.

MainWindow.xaml:

<Window x:Class="ShiftContextMenu.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate x:Key="DummyItemTemplate">
            <TextBlock Text="{Binding Name}">
                <TextBlock.ContextMenu>
                    <ContextMenu>
                        <MenuItem Command="{Binding SendToRecycleBin}" Header="Delete" />
                        <MenuItem Command="{Binding Delete}" Header="Permanently Delete" />
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
        </DataTemplate>
    </Window.Resources>
    <TreeView Name="tvMain" ItemTemplate="{StaticResource DummyItemTemplate}" ItemsSource="{Binding DummyItems}" />
</Window>

MainWindow.xaml.cs:

using System.Collections.Generic;
using System.Windows;
using System.Collections.ObjectModel;

namespace ShiftContextMenu
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            DummyItem[] dummyItems = new DummyItem[] {
                new DummyItem("First"),
                new DummyItem("Second"),
                new DummyItem("Third")
            };
            DummyItems = new ReadOnlyCollection<DummyItem>(new List<DummyItem>(dummyItems));
            this.DataContext = this;
            InitializeComponent();
        }

        public ReadOnlyCollection<DummyItem> DummyItems { get; protected set; }
    }
}

ViewModelBase.cs:

using System.ComponentModel;

namespace ShiftContextMenu
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        protected PropertyChangedEventHandler _propertyChangedEvent;

        protected void SendPropertyChanged(string propertyName)
        {
            if (_propertyChangedEvent != null)
            {
                _propertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged
        {
            add
            {
                _propertyChangedEvent += value;
            }
            remove
            {
                _propertyChangedEvent -= value;
            }
        }
    }
}

DummyItem.cs:

using System;
using System.Windows.Input;
using System.Windows;

namespace ShiftContextMenu
{
    public class DummyItem : ViewModelBase
    {
        public string Name { get; protected set; }

        public DummyItem(string name)
        {
            Name = name;
            _sendToRecycleBinCommand = new SendToRecycleBinCommand();
            _deleteCommand = new DeleteCommand();
        }

        protected SendToRecycleBinCommand _sendToRecycleBinCommand;
        protected DeleteCommand _deleteCommand;

        public ICommand SendToRecycleBin { get { return _sendToRecycleBinCommand; } }
        public ICommand Delete { get { return _deleteCommand; } }

        protected class SendToRecycleBinCommand : ICommand
        {
            public void Execute(object parameter)
            {
                MessageBox.Show("Send To Recycle Bin");
            }

            public bool CanExecute(object parameter)
            {
                return true;
            }

            public event EventHandler CanExecuteChanged { add { } remove { } }
        }

        protected class DeleteCommand : ICommand
        {
            public void Execute(object parameter)
            {
                MessageBox.Show("Permanently Delete");
            }

            public bool CanExecute(object parameter)
            {
                return true;
            }

            public event EventHandler CanExecuteChanged { add { } remove { } }
        }
    }
}

Upvotes: 0

Views: 2871

Answers (2)

Sean Hall
Sean Hall

Reputation: 7878

kmatyaszek's answer worked, but I didn't like having to modify my ViewModel. So I ended up creating the attached dependency property that I proposed in the question:

public static readonly DependencyProperty PowerUserOnlyProperty =
    DependencyProperty.RegisterAttached(
        "PowerUserOnly", 
        typeof(bool), 
        typeof(App), 
        new UIPropertyMetadata(false, new PropertyChangedCallback(PUOChanged)));

public static bool GetPowerUserOnly(MenuItem obj)
{
    return (bool)obj.GetValue(PowerUserOnlyProperty);
}

public static void SetPowerUserOnly(MenuItem obj, bool value)
{
    obj.SetValue(PowerUserOnlyProperty, value);
}

public static void PUOChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    MenuItem menuItem = sender as MenuItem;
    if (menuItem == null) return;

    bool value = (bool)e.NewValue;
    if (!value) return;

    new PowerUserOnlyHelper(menuItem);
}

public class PowerUserOnlyHelper
{
    public MenuItem Item { get; protected set; }

    public PowerUserOnlyHelper(MenuItem menuItem)
    {
        Item = menuItem;

        ContextMenu parent = VisualUpwardSearch<ContextMenu>(menuItem);
        if (parent != null)
        {
            parent.Opened += new RoutedEventHandler(OnContextMenuOpened);
        }
    }

    protected void OnContextMenuOpened(object sender, RoutedEventArgs e)
    {
        Visibility v;
        if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
        {
            v = Visibility.Visible;
        }
        else v = Visibility.Collapsed;

        Item.Visibility = v;
    }
}

public static T VisualUpwardSearch<T>(DependencyObject source)
    where T : DependencyObject
{
    DependencyObject returnVal = source;
    DependencyObject tempReturnVal;

    while (returnVal != null && !(returnVal is T))
    {
        tempReturnVal = null;
        if (returnVal is Visual || returnVal is Visual3D)
        {
            tempReturnVal = VisualTreeHelper.GetParent(returnVal);
        }
        if (tempReturnVal == null)
        {
            returnVal = LogicalTreeHelper.GetParent(returnVal);
        }
        else
        {
            returnVal = tempReturnVal;
        }
    }

    return returnVal as T;
}

Upvotes: 2

kmatyaszek
kmatyaszek

Reputation: 19296

Windows shell behaviour solution:

In this solution I'm using two assemblies:

  1. System.Windows.Interactivity
  2. System.Windows.Forms

Add following namespace to window:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

DataTemplate should look like this:

<DataTemplate x:Key="DummyItemTemplate">
            <TextBlock Text="{Binding Name}">
                <TextBlock.ContextMenu>
                    <ContextMenu> 
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="Opened">
                                <i:InvokeCommandAction Command="{Binding ShowMoreOptions}" />
                            </i:EventTrigger>
                            <i:EventTrigger EventName="Closed">
                                <i:InvokeCommandAction Command="{Binding HideMoreOptions}" />
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                        <MenuItem Command="{Binding SendToRecycleBin}" Header="Delete" />
                        <MenuItem Command="{Binding Delete}" Header="Permanently Delete">
                        <MenuItem.Style>
                            <Style TargetType="MenuItem">
                                <Setter Property="Visibility" Value="Collapsed" />
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding IsVisibleDelete}" Value="True">
                                        <Setter Property="Visibility" Value="Visible" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </MenuItem.Style>    
                    </MenuItem>
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
        </DataTemplate>

And in DummyItem class you must add two commands and one property:

private bool _isVisibleDelete;
public bool IsVisibleDelete
{
    get { return _isVisibleDelete; }
    set { _isVisibleDelete = value; SendPropertyChanged("IsVisibleDelete"); }
}

public ICommand ShowMoreOptions { get; private set; }
private void OnShowMoreOptions()
{
    if (System.Windows.Forms.Control.ModifierKeys == System.Windows.Forms.Keys.Shift)
        IsVisibleDelete = true;
}

public ICommand HideMoreOptions { get; private set; }
private void OnHideMoreOptions()
{
    IsVisibleDelete = false;
}

In my example I'm using DelegateCommand from Microsoft.Practices.Prism assembly.

So in ctor DummyItem I have:

 ShowMoreOptions = new DelegateCommand(this.OnShowMoreOptions);
 HideMoreOptions = new DelegateCommand(this.OnHideMoreOptions);

Second solution allow you change this menu dynamically:

You can try something like that:

XAML file:

<TextBlock>
            <TextBlock.ContextMenu>
                <ContextMenu>
                    <ContextMenu.InputBindings>
                        <KeyBinding Modifiers="Shift" Key="Shift" Command="{Binding ShowMoreOptions}" />
                    </ContextMenu.InputBindings>
                    <MenuItem Command="{Binding SendToRecycleBin}" Header="Delete" />
                    <MenuItem Command="{Binding Delete}" Header="Permanently Delete">
                        <MenuItem.Style>
                            <Style TargetType="MenuItem">
                                <Setter Property="Visibility" Value="Collapsed" />
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding IsVisibleDelete}" Value="True">
                                        <Setter Property="Visibility" Value="Visible" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </MenuItem.Style>    
                    </MenuItem>
                </ContextMenu>
            </TextBlock.ContextMenu>
        </TextBlock>

And in your ViewModel class you should add property and command to change MenyItem visibility property:

private bool _isVisibleDelete;
public bool IsVisibleDelete
{
    get { return _isVisibleDelete; }
    set { _isVisibleDelete = value; RaisePropertyChanged(() => IsVisibleDelete); }
}

public ICommand ShowMoreOptions { get; private set; }
private void OnShowMoreOptions()
{
    IsVisibleDelete = !IsVisibleDelete;
}

Upvotes: 2

Related Questions