Flyouts in WinRT are a way for you to add additional information to FrameworkElements in a modal way.
They can be used for various reasons :
- Showing feedback when an element gets focus, or any event takes place
- To replace the ComboBox control, by adding a ListView to select an item
- To replace the MessageDialog control so you can add a custom interface
Now there are these drawbacks on using the Flyout :
- Appbar of the page does not automatically hides when showing a Flyout in Full Placement
- Flyout does not close when an item is selected
- Nested listviews are not scrollable inside the Flyout
What we will achieve :
How this is setup:
- Give the root element the name “layoutRoot”
- Create two behaviors (FlyoutFullPageBehavior and AppBarFlyoutBehavior)
- Add a reference to the behaviors namespace in your page
- Implement the XAML snippet below
<Button.Flyout> <Flyout Placement="Full"> <i:Interaction.Behaviors> <behaviors:FlyoutFullPageBehavior Parent="{Binding ElementName=layoutRoot}" /> <behaviors:AppBarFlyoutBehavior Parent="{Binding ElementName=layoutRoot}" /> </i:Interaction.Behaviors> <!-- Flyout Content here --> </Flyout> </Button.Flyout>
AppBarFlyoutBehavior.cs
public class AppBarFlyoutBehavior : DependencyObject, IBehavior { private Flyout element = null; public DependencyObject AssociatedObject { get { return element; } } public FrameworkElement Parent { get { return GetValue(ParentProperty) as FrameworkElement; } set { SetValue(ParentProperty, value); } } // Using a DependencyProperty as the backing store for EmptyDataTemplate. This enables animation, styling, binding, etc... public static readonly DependencyProperty ParentProperty = DependencyProperty.Register("Parent", typeof(FrameworkElement), typeof(AppBarFlyoutBehavior), new PropertyMetadata(null, ParentPropertyChanged)); private static void ParentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var behavior = (AppBarFlyoutBehavior)d; behavior.Parent = (FrameworkElement)e.NewValue; } public void Attach(DependencyObject associatedObject) { if (associatedObject as Flyout == null) throw new NotSupportedException("The associated object must be of type Flyout"); element = associatedObject as Flyout; element.Opened += FlyoutOpened; element.Closed += FlyoutClosed; } private void FlyoutOpened(object sender, object e) { ShowAppBar(false); } private void FlyoutClosed(object sender, object e) { ShowAppBar(true); } private void ShowAppBar(bool show) { if (Parent != null) { var page = ControlHelper.GetParentPage(Parent); if (page != null && page.BottomAppBar != null) { if (show) page.BottomAppBar.Visibility = Visibility.Visible; else page.BottomAppBar.Visibility = Visibility.Collapsed; } } } public void Detach() { element.Opened -= FlyoutOpened; element.Closed -= FlyoutClosed; } }
FlyoutFullPageBehavior.cs
public class FlyoutFullPageBehavior : DependencyObject, IBehavior { private Flyout element = null; private double correction = 26; public DependencyObject AssociatedObject { get { return element; } } public FrameworkElement Parent { get { return GetValue(ParentProperty) as FrameworkElement; } set { SetValue(ParentProperty, value); } } // Using a DependencyProperty as the backing store for EmptyDataTemplate. This enables animation, styling, binding, etc... public static readonly DependencyProperty ParentProperty = DependencyProperty.Register("Parent", typeof(FrameworkElement), typeof(FlyoutFullPageBehavior), new PropertyMetadata(null, ParentPropertyChanged)); private static void ParentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var behavior = (FlyoutFullPageBehavior)d; behavior.Parent = (FrameworkElement)e.NewValue; } public void Attach(DependencyObject associatedObject) { if (associatedObject as Flyout == null) throw new NotSupportedException("The associated object must be of type Flyout"); element = associatedObject as Flyout; element.Opened += FlyoutOpened; } private void FlyoutOpened(object sender, object e) { if (element.Placement == FlyoutPlacementMode.Full) { Style flyoutPresenterStyle = Application.Current.Resources["FlyoutFullPageBehaviorStyle"] as Style; if (flyoutPresenterStyle == null) flyoutPresenterStyle = new Style(typeof(FlyoutPresenter)); var scrollSetter = new Setter(ScrollViewer.VerticalScrollModeProperty, ScrollMode.Disabled); if (flyoutPresenterStyle.Setters.Contains(scrollSetter)) flyoutPresenterStyle.Setters.Add(scrollSetter); element.FlyoutPresenterStyle = flyoutPresenterStyle; var content = element.Content as FrameworkElement; var page = ControlHelper.GetParentPage(Parent); if (page != null) { page.SizeChanged += PageSizeChanged; content.HorizontalAlignment = HorizontalAlignment.Stretch; content.VerticalAlignment = VerticalAlignment.Stretch; AppyHeightToFlyoutContent(element, page.ActualHeight, correction); } } } private void AppyHeightToFlyoutContent(Flyout flyout, double height, double correction) { var content = flyout.Content as FrameworkElement; if (content != null) content.Height = height - correction; } private void PageSizeChanged(object sender, SizeChangedEventArgs e) { var page = sender as Page; AppyHeightToFlyoutContent(element, page.ActualHeight, correction); } public void Detach() { element.Opened -= FlyoutOpened; var page = ControlHelper.GetParentPage(Parent); if (page != null) { page.SizeChanged -= PageSizeChanged; } } }
The flyout can be styled using the following XAML style
<Style x:Key="FlyoutFullPageBehaviorStyle" TargetType="FlyoutPresenter"> <Setter Property="Foreground" Value="Red" /> <Setter Property="Background" Value="Green" /> </Style>
Using this setup, you’ll be now able to show full page flyouts as if they where pop-ups. The appbar will become hidden when the flyout is visible.
Lets say we want to hide the flyout after an event has happened, a selection event in a listview, for example.
Then we could use the following behavior to close the flyout upon the click event of a listview.
Consider the following flyout for a button :
<Button> <Button.Flyout> <Flyout Placement="Full"> <i:Interaction.Behaviors> <behaviors:FlyoutFullPageBehavior Parent="{Binding ElementName=layoutRoot}" /> <behaviors:AppBarFlyoutBehavior Parent="{Binding ElementName=layoutRoot}" /> </i:Interaction.Behaviors> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Margin="20,12,0,0" Text="Select an item below :" /> <ListView Grid.Row="1" IsItemClickEnabled="True" ItemsSource="{Binding Items, Mode=OneWay}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"> <i:Interaction.Behaviors> <behaviors:FlyoutListViewItemClickBehavior ListItemClickedCommand="{Binding ItemSelectedCommand}" /> </i:Interaction.Behaviors> <ListView.ItemContainerStyle> <Style TargetType="ListViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> </Style> </ListView.ItemContainerStyle> </ListView> </Grid> </Flyout> </Button.Flyout> </Button>
BehaviorBase.cs
public abstract class BehaviorBase<T> : DependencyObject, IBehavior where T : FrameworkElement { private T element; /// <summary /// !Important! /// Whenever you define Dependency Properties on Behaviors (or any DependencyObject for that matter), you need to set a private static function to act on PropertyMetadata changed. /// Since this is static, this can be executed multiple times from DependencyObjects that are not part of the main UI thread anymore /// So whenever you want to change something on the UI thread or push changes of bindingexpressions to the source, you need to surround it with an if-check: /// if(IsActive){ /// //your UI and bindingexpression code here /// } /// </summary> private bool isActive = false; public bool IsActive { get { return isActive; } set { isActive = value; } } public T AssociatedElement { get { return element; } } public DependencyObject AssociatedObject { get { return element; } } public void Attach(DependencyObject associatedObject) { if (associatedObject as T == null) throw new NotSupportedException("The associated object must be of type " + typeof(T).Name); element = associatedObject as T; ElementAttached(); } protected virtual void ElementAttached() { element.Loaded += ElementLoaded; isActive = true; } protected virtual void ElementLoaded(object sender, RoutedEventArgs e) { var page = ControlHelper.GetParentPage(element); if (page != null && page.NavigationCacheMode == NavigationCacheMode.Disabled)//Subcribe to the unloading event to handle state page.Unloaded += PageUnloaded; } private void PageUnloaded(object sender, RoutedEventArgs e) { this.Detach(); } public void Detach() { ElementDetached(); isActive = false; } protected virtual void ElementDetached() { var page = ControlHelper.GetParentPage(element); if (page != null && page.NavigationCacheMode == NavigationCacheMode.Disabled)//Subcribe to the unloading event to handle state page.Unloaded -= PageUnloaded; element.Loaded -= ElementLoaded; } }
ControlHelper.cs
public static class ControlHelper { public static Page GetParentPage(FrameworkElement elem) { var parent = elem.Parent; var parentPage = (parent as Page); while (parentPage == null && parent != null) { parent = (parent as FrameworkElement).Parent; parentPage = (parent as Page); } return parentPage; } public static T FindParent<T>(DependencyObject element) where T : FrameworkElement { FrameworkElement parent = VisualTreeHelper.GetParent(element) as FrameworkElement; while (parent != null) { T correctlyTyped = parent as T; if (correctlyTyped != null) return correctlyTyped; else return FindParent<T>(parent); } return null; } public static T FindChild<T>(FrameworkElement element) where T : FrameworkElement { int childCount = VisualTreeHelper.GetChildrenCount(element); for (int c = 0; c < childCount; c++) { FrameworkElement child = VisualTreeHelper.GetChild(element, c) as FrameworkElement; if (child != null) { T correctlyTyped = child as T; if (correctlyTyped != null) return correctlyTyped; else return FindChild<T>(child); } } return null; } }
FlyoutListViewItemClickBehavior.cs
public class FlyoutListViewItemClickBehavior : BehaviorBase<ListViewBase> { public event EventHandler ItemClicked; protected override void ElementAttached() { base.ElementAttached(); if (!AssociatedElement.IsItemClickEnabled) throw new NotSupportedException("Please set IsItemClickEnabled to true on the ListView!"); AssociatedElement.ItemClick += listView_ItemClick; } protected override void ElementDetached() { base.ElementDetached(); AssociatedElement.ItemClick -= listView_ItemClick; } private void listView_ItemClick(object sender, ItemClickEventArgs e) { var parentFlyout = ControlHelper.FindParent<FlyoutPresenter>(AssociatedElement); if (parentFlyout != null) { if (parentFlyout.Parent is Popup) { Popup popUp = parentFlyout.Parent as Popup; AssociatedElement.SelectedItem = e.ClickedItem;//Trigger the SelectedItem binding popUp.IsOpen = false;//close the popup if (ListItemClickedCommand != null) ListItemClickedCommand.Execute(e.ClickedItem); if (ItemClicked != null) ItemClicked(e.ClickedItem, EventArgs.Empty); } } } public ICommand ListItemClickedCommand { get { return (ICommand)base.GetValue(FlyoutListViewItemClickBehavior.ListItemClickedCommandProperty); } set { base.SetValue(FlyoutListViewItemClickBehavior.ListItemClickedCommandProperty, value); } } // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty ListItemClickedCommandProperty = DependencyProperty.Register("ListItemClickedCommand", typeof(ICommand), typeof(FlyoutListViewItemClickBehavior), new PropertyMetadata(null, OnListItemClickedCommandPropertyChanged)); private static void OnListItemClickedCommandPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var behavior = (FlyoutListViewItemClickBehavior)d; if (behavior.IsActive) behavior.ListItemClickedCommand = (ICommand)e.NewValue; } } }
Source code (rename to .zip to unpack) :
FlyoutApp