I was reading this article and could not help but wonder the same thing.
Is there a way to databind a menu Flyout
control?
I was reading this article and could not help but wonder the same thing.
Is there a way to databind a menu Flyout
control?
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:
I hope this helps you.
Best of luck!
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.
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);
}
}
}