Scrolling ContextMenu items

2020-05-01 08:33发布

问题:

In Windows Phone there is a feature called ContextMenu, which allows to create a popup menu for controls.

However, if the list of menu items is quite big, some of them doesn't fit the screen. The following simple example will show the point:

in xaml:

We will be using Toolkit, so adding

xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"

and then for example:

   <Button Content="Button 1" Height="72" HorizontalAlignment="Left" Margin="160,400,0,0" Name="button1" VerticalAlignment="Top" Width="160">
        <toolkit:ContextMenuService.ContextMenu>
            <toolkit:ContextMenu>                    
                    <toolkit:MenuItem Header="Action A" Name="miActionA1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action B" Name="miActionB1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action C" Name="miActionC1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action D" Name="miActionD1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action E" Name="miActionE1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action F" Name="miActionF1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />                      
            </toolkit:ContextMenu>
        </toolkit:ContextMenuService.ContextMenu>
    </Button>

At nullAction we're doing nothing

private void nullAction(object sender, RoutedEventArgs e)
        {

        }

The DesignHeight value is standard for the default WP7 page it's DesignHeight="768"

As you can see, when you long tap the "Button 1" control, the menu is shown, but not completely. Some items are not visible. In my case, the best decision was to make this menu scrollable. That can be done, using ScrollViewer.

So, we cover the menu items with Scrollviewer and StackPanel.

 <Button Content="Button 1" Height="72" HorizontalAlignment="Left" Margin="160,400,0,0" Name="button1" VerticalAlignment="Top" Width="160">
        <toolkit:ContextMenuService.ContextMenu>
            <toolkit:ContextMenu>
                <ScrollViewer>
                    <StackPanel>
                    <toolkit:MenuItem Header="Action A" Name="miActionA1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action B" Name="miActionB1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action C" Name="miActionC1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action D" Name="miActionD1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action E" Name="miActionE1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    <toolkit:MenuItem Header="Action F" Name="miActionF1" BorderBrush="Black" BorderThickness="1" Click="nullAction" />
                    </StackPanel>
                </ScrollViewer>
            </toolkit:ContextMenu>
        </toolkit:ContextMenuService.ContextMenu>
    </Button>

Success? Nope, because you can scroll this list, but can't choose the item down.

My question, is how to fix it?

Use multitouch? (I don't know much about it)

Perhaps, I need to add some property to Scrolviewer?

Any other ideas?

[updated] Also, found that tap/click handlers for items change behavior, if scrollviewer is added. Normally, when you tap the item, the context menu closes and the handler method executes. With scrollviewer, the context menu doesn't close, needs one more tap to close it manually. Why's that happening?

回答1:

If the native ContextMenu doesn't allow this, then, the best way to solve a problem, is to write your own implementation.

The new context menu should be shown in a popup with a choice of several items, and that items must be scrollable. So:

public class ScrollableContextMenu
    {
        private Popup p;
        public delegate void TapHandler(object sender, System.Windows.Input.GestureEventArgs e);
        public event TapHandler ListBoxTap;

        private ListBox listBox;

        public ListBox ListBox
        {
            get { return listBox; }
            set { listBox = value; }
        }

        /// <summary>
        /// Create new Context Menu. The items of Context Menu will be taken from given list
        /// </summary>
        /// <param name="page"></param>
        /// <param name="items"></param>
        public ScrollableContextMenu(PhoneApplicationPage page, List<string> items)
        {
            p = new Popup();

           // Generate popup properties, i.e. height, width, e.t.c.

            Canvas canvas = new Canvas();

            // Generate canvas main properties

            p.Child = canvas;

            Border border = new Border();

            // Now create border and its main properties

            canvas.Children.Add(border);

            // StackPanel.
            StackPanel panel = new StackPanel();
            panel.Orientation = System.Windows.Controls.Orientation.Vertical;

            // Create listBox, that we will be scrolling
            listBox = new ListBox();
            // Create listbox main properties

            // Fill the listbox with items. 
            foreach (string item in items)
                listBox.Items.Add(new ScrollableContextMenuItem(item));
            listBox.Tap += listBoxTap;
            panel.Children.Add(listBox);
            border.Child = panel;
        }

        public void Show()
        {
            // Open the popup.
            p.IsOpen = true;
            p.UpdateLayout();
        }

        public void Close()
        {
            // Close it
            p.IsOpen = false;
            p.UpdateLayout();
        }

        private void listBoxTap(object sender,  System.Windows.Input.GestureEventArgs e)
        {
            // Invoke hanlder if it exists
            if (ListBoxTap != null)
                ListBoxTap(sender, e);
        }

The ScrollableContextMenuItem is a UserControl, which should look like native ContextMenuItem. In short, it's a simple TextBox over Grid/StackPanel.

The native ContextMenu is added to the xaml file as

<toolkit:ContextMenuService.ContextMenu>
        <toolkit:ContextMenu>
               <toolkit:MenuItem Header="..." ... Click="miClickEvent"/>
                <!-- ... --!>
        </toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>

Instead, the ScrollableContextMenu object should be called via Hold event:

button.Hold += new EventHandler<System.Windows.Input.GestureEventArgs>(SomeHoldEvent);



 private void SomeHoldEvent(object sender, EventArgs e)
        {
            contextMenu = new ScrollableContextMenu(this, definedList);
            contextMenu.ListBoxTap +=new ScrollableContextMenu.TapHandler(contextMenu_ListBoxTap);
            contextMenu.Show();
        }

Where the defined list is a List of headers for context menu (should be generated somewhere).

At listBoxTap event:

private void contextMenu_ListBoxTap(object sender, EventArgs e)
        {
            int index = contextMenu.ListBox.SelectedIndex;
            switch (index)
            {
                // items index starts with zero
                case 0:
                    // Call the click event for this one, it should be implemented already, when we've written it for native context menu
                    MiClickEvent(this, EventArgs.Empty);
                    contextMenu.Close();
                    break;
                // implement other handlers here
                default:
                    contextMenu.Close();
                    break;
            }

That's the only solution, that really helped me. Hope it'll help someone else.