Why doesn't the Windows 8.1 MenuFlyout have ItemsSource property?
Asked Answered
F

3

11

I was reading this article and could not help but wonder the same thing.

Is there a way to databind a menu Flyout control?

Fifteenth answered 2/12, 2013 at 19:44 Comment(1)
Here's the answer blog.jerrynixon.com/2013/12/…Fifteenth
F
12

Yes.

I put together a simple solution for developers who desire this functionality. It uses an attached property to identify the ItemsSource and the ItemTemplate for a Flyout control. If the developer elects to use a MenuFlyoutItem or something else, it is up to them.

Here's the attached property:

public class BindableFlyout : DependencyObject
{
    #region ItemsSource

    public static IEnumerable GetItemsSource(DependencyObject obj)
    {
        return obj.GetValue(ItemsSourceProperty) as IEnumerable;
    }
    public static void SetItemsSource(DependencyObject obj, IEnumerable value)
    {
        obj.SetValue(ItemsSourceProperty, value);
    }
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.RegisterAttached("ItemsSource", typeof(IEnumerable),
        typeof(BindableFlyout), new PropertyMetadata(null, ItemsSourceChanged));
    private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    { Setup(d as Windows.UI.Xaml.Controls.Flyout); }

    #endregion

    #region ItemTemplate

    public static DataTemplate GetItemTemplate(DependencyObject obj)
    {
        return (DataTemplate)obj.GetValue(ItemTemplateProperty);
    }
    public static void SetItemTemplate(DependencyObject obj, DataTemplate value)
    {
        obj.SetValue(ItemTemplateProperty, value);
    }
    public static readonly DependencyProperty ItemTemplateProperty =
        DependencyProperty.RegisterAttached("ItemTemplate", typeof(DataTemplate),
        typeof(BindableFlyout), new PropertyMetadata(null, ItemsTemplateChanged));
    private static void ItemsTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    { Setup(d as Windows.UI.Xaml.Controls.Flyout); }

    #endregion

    private static async void Setup(Windows.UI.Xaml.Controls.Flyout m)
    {
        if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
            return;
        var s = GetItemsSource(m);
        if (s == null)
            return;
        var t = GetItemTemplate(m);
        if (t == null)
            return;
        var c = new Windows.UI.Xaml.Controls.ItemsControl
        {
            ItemsSource = s,
            ItemTemplate = t,
        };
        var n = Windows.UI.Core.CoreDispatcherPriority.Normal;
        Windows.UI.Core.DispatchedHandler h = () => m.Content = c;
        await m.Dispatcher.RunAsync(n, h);
    }
}

And, here's sample usage.

<Page.BottomAppBar>
    <CommandBar>
        <AppBarButton Label="AppBarButton">
            <AppBarButton.Flyout>
                <Flyout local:BindableFlyout.ItemsSource="{Binding MenuItems}">
                    <local:BindableFlyout.ItemTemplate>
                        <DataTemplate>
                            <MenuFlyoutItem Text="{Binding Text}" />
                        </DataTemplate>
                    </local:BindableFlyout.ItemTemplate>
                </Flyout>
            </AppBarButton.Flyout>
            <AppBarButton.Icon>
                <SymbolIcon/>
            </AppBarButton.Icon>
        </AppBarButton>
    </CommandBar>
</Page.BottomAppBar>

I will be maintaining this code here.

Looks like this:

enter image description here

I hope this helps you.

Best of luck!

Fifteenth answered 2/12, 2013 at 19:44 Comment(6)
I'm using your sample code, but I'm finding that the menu no longer dismisses itself after an item is clicked. Any ideas?Natica
oh I think this may be because this is a Flyout and not a MenuFlyout, I'll see whether I can work something out from your blog. thanksNatica
Using a ChangePropertyAction Blend Behavior you can simply set the Flyout.IsOpen property to false when the MenuItem is clicked. Make sense? Of course you could do this in code-behind, too. Either way works. You could also have a MenuOpen property in your ViewModel if you want to do it there. Lots of options.Fifteenth
ChangePropertyAction doesn't work because it's actually a method called Hide. CallMethodAction doesn't work because you cannot do an ElementName Binding inside the DataTemplate of BindableFlyout, so you cannot call the parent Flyout's Hide method. In the end I opted for using code-behind and events from the view model.Natica
How could I get this to work with a SelectedItem? Meaning the MenuItem triggers calling control to change based off of what I've selected in this bindable Flyout?Fowliang
Is there a way to make this work with ItemTemplateSelector?Selfassured
A
1

Even though the original question was asked ages ago I'll post the solution I've found, as someone else might find it useful.

Jerry's solution has a serious flaw: the MenuFlyout isn't closed when you click an item and I've found it exceedingly difficult to do so, as it seems to be (nearly?) impossible to get a reference to the Flyout from inside the DataTemplate to close it.

I've come up with this solution that subclasses MenuFlyout:

public class BindableFlyout : MenuFlyout
{
    public ICollection<ContextMenuCommand> ItemsSource
    {
        get { return (ICollection<ContextMenuCommand>)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(ICollection<ContextMenuCommand>), typeof(BindableFlyout), new PropertyMetadata(null, (DependencyObject o, DependencyPropertyChangedEventArgs args) =>
        {
            Setup(o as BindableFlyout);
        }
    ));

    private static async void Setup(BindableFlyout menuFlyout)
    {
        if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
            return;
        if (menuFlyout.ItemsSource == null)
            return;

        await menuFlyout.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
        {
            menuFlyout.Items.Clear();
            foreach (var menuItem in menuFlyout.ItemsSource)
            {
                menuFlyout.Items.Add(new MenuFlyoutItem()
                {
                    Text = menuItem.Text,
                    Command = menuItem.Command
                });
            }
        });
    }
}

public class ContextMenuCommand
{
    public ContextMenuCommand(ICommand command, string text)
    {
        Command = command;
        Text = text;
    }

    public string Text
    {
        get; private set;
    }

    public ICommand Command
    {
        get; private set;
    }
}

The snippet above doesn't listen to changes of the ItemsSource, but you can easily adapt the class.

Asante answered 11/2, 2016 at 19:36 Comment(0)
S
0

It works for me. I hope I didn't miss a thing.

 class CustomCommand : ICommand
    {
        public ICommand CommandObject { get { return this; } }
        public String CommandName { get; private set; }

        public CustomCommand(String name):base()
        {
            this.CommandName = name;
        }
    }

    class EncapsulateOrDecoratorObjectForContextMenu
    {
        private object baseObject;
        // chaned properties to the baseObject

        public List<CustomCommand> AvailableCommands { get; set; }

        public EncapsulateOrDecoratorObjectForContextMenu(object baseObject, List<CustomCommand> commands)
        {
            this.baseObject = baseObject;
            this.AvailableCommands = commands;
        }

    }

    class SomePage: Page
    {
        private MenuFlyout mFlyout;

        public SomePage()
        {
            // I don't know why, but it's to be here... unless UI/design go crazy
            this.mFlyout = new MenuFlyout();
        }

        private void Grid_Holding(object sender, HoldingRoutedEventArgs e)
        {
            if (e.OriginalSource is FrameworkElement && (e.OriginalSource as FrameworkElement).DataContext is EncapsulateOrDecoratorObjectForContextMenu)
            {
                // Only the property is 'readonly', not the List<menuItem> itself, so...
                this.mFlyout.Items.Clear();

                MenuFlyoutItem menuItem;
                foreach (CustomCommand command in ((e.OriginalSource as FrameworkElement).DataContext as EncapsulateOrDecoratorObjectForContextMenu).AvailableCommands)
                {
                    menuItem = new MenuFlyoutItem();
                    menuItem.Text = command.CommandName;
                    menuItem.Command = command.CommandObject;

                    this.mFlyout.Items.Add(menuItem);
                }

                FrameworkElement senderElement = sender as FrameworkElement;
                this.mFlyout.ShowAt(senderElement);
            }
        }
    }
Slivovitz answered 20/2, 2015 at 15:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.