This post describes the design of the CharmFlyout
custom control, discussing both the C# and XAML that achieves the desired functionality.
Posts in this series:
CharmFlyout is a custom control with the following dependency properties:
bool IsOpen
– Set this to true
to show the flyout, and false
to close the flyout. What could be easier? double FlyoutHeight
– This read-only property is automatically set by CharmFlyout
to match the height of the screen. double FlyoutWidth
– Set this to either 346 (default) or 646 to remain in compliance with the Metro style. Of course, you could be a rebel and set it to 347 – no one would know. string Heading
– Set this to the text you want displayed on the top of the flyout. ICommand BackCommand
– This read-only property allows CharmFlyout
to respond to the user clicking the back arrow. Brush HeadingForegroundBrush
– White by default. Use whatever color works best for your application. Brush HeadingBackgroundBrush
– Black by default. Brush ContentForegroundBrush
– Black by default. Brush ContentBackgroundBrush
– White by default. CharmFlyout ParentFlyout
– If this CharmFlyout
is a sub-flyout, then set this to the parent CharmFlyout
.
It would be tedious to paste all the code that implements these properties. They really are nothing special. Because CharmFlyout
derives from ContentControl
, it also inherits many other dependency properties. One important property is:
object Content
– This should contain the user interface elements that make up the bulk of the flyout content.
So then, all that is left to discuss regarding CharmFlyout
’s C# code is this:
CharmFlyout.cs
[ContentProperty(Name = "Content")]
public sealed class CharmFlyout : ContentControl
{
public CharmFlyout()
{
DefaultStyleKey = typeof(CharmFlyout);
FlyoutWidth = 346;
BackCommand = new RelayCommand(OnBack);
this.SizeChanged += OnSizeChanged;
HeadingBackgroundBrush = new SolidColorBrush(Colors.Black);
HeadingForegroundBrush = new SolidColorBrush(Colors.White);
ContentBackgroundBrush = new SolidColorBrush(Colors.White);
ContentForegroundBrush = new SolidColorBrush(Colors.Black);
}
void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
FlyoutHeight = e.NewSize.Height;
}
private void OnIsOpenChanged()
{
if (ParentFlyout != null && IsOpen)
{
ParentFlyout.IsOpen = false;
}
}
private void OnBack(object obj)
{
IsOpen = false;
if (ParentFlyout != null)
{
ParentFlyout.IsOpen = true;
}
else
{
SettingsPane.Show();
}
}
The first line (ContentProperty
) is simply a convenience for the user of this control. It specifies which dependency property should be set if the user simply adds content to the XAML of this control. For example, without the line, the user would have to add their own content to the flyout like this:
<cfo:CharmFlyout>
<cfo:CharmFlyout.Content>
<TextBlock
Text="My Content" />
</cfo:CharmFlyout.Content>
</cfo:CharmFlyout>
With the line, the user can add their own content with less clutter:
<cfo:CharmFlyout>
<TextBlock
Text="My Content" />
</cfo:CharmFlyout>
The next line of interest is the one:
DefaultStyleKey = typeof(CharmFlyout);
Setting the DefaultStyleKey
causes the style “local:CharmFlyout
” defined in Generic.xaml to be associated with this custom control. I will show the entire style a bit later in this post. For now, just know that there is a back button defined in the style that looks like this:
<Button
Command="{TemplateBinding BackCommand}"
When the user clicks this button, we want to set IsOpen
to false
. We also want to re-show the settings pane if this is a root-level flyout. If this is a sub flyout, we want to re-show the parent flyout. Since the Command handler for the button is already bound to the BackCommand
dependency property in CharmFlyout
, we just need to associate this code with BackCommand
:
private void OnBack(object obj)
{
IsOpen = false;
if (ParentFlyout != null)
{
ParentFlyout.IsOpen = true;
}
else
{
SettingsPane.Show();
}
}
For more information on RelayCommand
, see Josh Smith’s WPF Apps With The Model-View-ViewModel Design Pattern.
To ensure that the height of the flyout PopUp tracks the height of the window that contains CharmFlyout
, we respond to changes in the window size of this custom control by setting FlyoutHeight
. The Popup in Generic.xaml then binds to this height.
By the way, it would seem that we could dispense with FlyoutHeight
and just bind to ActualHeight
. However, in practice that does not seem to work very well in Metro. I have not looked into why this does not work.
Probably now is as good a time as any to show some portions of Generic.xaml. First, the back button is a work of art. It is a 50x50 sized target with a circle and an arrow. It took a while to get the font size and margins just right. Compared to this, the style for the CharmFlyout
is downright boring. It consists of a PopUp with various embedded borders and grids that eventually make up the entire flyout.
Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CharmFlyoutLibrary">
<Style
x:Key="BackButtonStyle"
TargetType="Button">
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid
Width="50"
Height="50">
<TextBlock
Text="?"
FontFamily="Segoe UI Symbol"
FontSize="41"
Margin="8,-5,-8,5" />
<TextBlock
Text="?"
FontFamily="Segoe UI Symbol"
FontSize="16"
Margin="16,14,-16,-14" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
TargetType="local:CharmFlyout">
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="local:CharmFlyout">
<Popup
Width="{TemplateBinding FlyoutWidth}"
Height="{TemplateBinding FlyoutHeight}"
IsOpen="{TemplateBinding IsOpen}"
IsLightDismissEnabled="True"
HorizontalAlignment="Right">
<Border
Width="{TemplateBinding FlyoutWidth}"
Height="{TemplateBinding FlyoutHeight}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="80" />
<RowDefinition
Height="*" />
</Grid.RowDefinitions>
<Border
Background="{TemplateBinding HeadingBackgroundBrush}">
<StackPanel
Margin="29,19,5,0"
Orientation="Horizontal">
<StackPanel.Transitions>
<TransitionCollection>
<EntranceThemeTransition />
</TransitionCollection>
</StackPanel.Transitions>
<Button
Command="{TemplateBinding BackCommand}"
Style="{StaticResource BackButtonStyle}"
Foreground="{TemplateBinding HeadingForegroundBrush}" />
<TextBlock
Margin="0,10,0,5"
Text="{TemplateBinding Heading}"
VerticalAlignment="Top"
FontFamily="Segoe UI"
FontSize="28"
FontWeight="Thin"
LineHeight="30"
Foreground="{TemplateBinding HeadingForegroundBrush}" />
</StackPanel>
</Border>
<Grid
Grid.Row="1"
Background="{TemplateBinding ContentBackgroundBrush}">
<Rectangle
Fill="{TemplateBinding ContentForegroundBrush}"
Width="1"
HorizontalAlignment="Left" />
<Border
Margin="40,26,30,30">
<ContentPresenter
Foreground="{TemplateBinding ContentForegroundBrush}">
<ContentPresenter.Transitions>
<TransitionCollection>
<EntranceThemeTransition />
</TransitionCollection>
</ContentPresenter.Transitions>
</ContentPresenter>
</Border>
</Grid>
</Grid>
</Border>
</Popup>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
TargetType="local:CharmFrame">
<Setter
Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter
Property="IsTabStop"
Value="False" />
<Setter
Property="VerticalContentAlignment"
Value="Stretch" />
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="local:CharmFrame">
<Grid>
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<ContentPresenter
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ContentPresenter
Content="{TemplateBinding CharmContent}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Here is a comparison of the CharmFlyout vs. the Permissions flyout: