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