Pages

Thursday, July 5, 2012

CharmFlyout – The Design

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 a 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; // or 646
        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="&#xE0A7;"
                           FontFamily="Segoe UI Symbol"
                           FontSize="41"
                           Margin="8,-5,-8,5" />
                        <TextBlock
                           Text="&#xE112;"
                           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:

ap

CharmFlyout binaries reside here: http://nuget.org/packages/charmflyout/

CharmFlyout source resides here: http://charmflyout.codeplex.com/

13 comments:

  1. Thanks for the control; I appreciate it! Is there any way to get access to the XAML source of it? I would like to customize a few things, such as the placement of the Back button, and the font size and positioning of the Header.

    - Alex

    ReplyDelete
  2. Alex,

    Since CharmFlyout is a templated-control, you can define your own style without the source. Blend is a great tool to create a copy of the style and edit it. If that does not work for you, the full source (including generic.xaml) is at http://charmflyout.codeplex.com/. If you have suggestions for a more Metro-compliant set of placement and font sizes, let me know and I would love to make those changes and republish. As it is, it seems to be very close to the Permissions flyout. Take care.

    ReplyDelete
  3. Awesome, thank you! Just what I was searching for. I wonder why this is not part of the winrt SDK.

    ReplyDelete
  4. There are a few things that don't match the UI guidelines and personality here (as well as light dismiss behavior). See http://timheuer.com/blog/archive/2012/05/31/introducing-callisto-a-xaml-toolkit-for-metro-apps.aspx for an implementation matching them. For @Anonymous who wants to change the location and sizes of things in the header, this is not recommended as it would be good to be consistent with the UI guidelines for consistency. For @Beave - there will be an SDK sample of this for RTM

    ReplyDelete
    Replies
    1. Tim,

      Thanks for pointing out the margin discrepancies. I updated CharmFlyout and the look is now identical (as far as I can tell) with the MSFT supplied Permissions flyout. As for the light-dismiss behavior, CharmFlyout behaves identically to the MSFT Permissions flyout - so I'm not sure what to change in that regard. Any specifics you might offer are more than welcome.

      Delete
  5. Hi, John

    The control is great, and I am trying to use it in my little project, thank you guys for sharing such good stuff.

    I think I need you help:

    I encountered weird issue, WebView covers CharmFlyout UI.

    The WebView is on the right of current page, when I click settings to bring up the CharmFlyout UI, The WebView control shows on top of CharmFlyout (only the overlapped part, the other part of CharmFlyout UI shows normally), any idea?

    I checked the code in App.xaml.cs, I did use CharmFrame as rootFrame.

    Thanks in advance.

    ReplyDelete
    Replies
    1. WebView's "airspace" issue seems to be a real mess. I cannot offer anything beyond what is written here: http://stackoverflow.com/questions/10639493/how-to-solve-z-index-of-webview-control-in-xaml-in-metro-style-app

      I suppose beyond using a brush, you could always collapse your WebView. Sorry I cannot be of more help.

      It looks like MSFT states "There are no planned changes for Windows 8 regarding WebView pertaining to [the airspace issue]..."
      - http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/23a235a2-b0a3-4161-bc74-c42289c61264

      Delete
    2. Did you find another solution to your problem? If so, please let me know a strategy for solving the problem. I would like to try and incorporate a solution in CharmFlyout.

      Delete
  6. Hi John

    I want to write the event on backbutton of the charmflyout so how to use BackCommand to do it, I am using xaml of CharmFlyout so how to add the BackCommand on its code behind, I need your help.

    Thanks in advance
    Reply

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Anonymous,

      I just released a new version of CharmFlyout that supports your needs. You can now subscribe to IsOpenChanged to be notified when the flyout is opened and closed, regardless of whether the back button was pressed or it was light-dismissed.

      public MainPage()
      {
      this.InitializeComponent();
      SettingsPane.GetForCurrentView().CommandsRequested += CommandsRequested;
      cfoSettings.IsOpenChanged += CfoSettingsIsOpenChanged;
      }

      void CfoSettingsIsOpenChanged(object sender, EventArgs e)
      {
      isOpenTextBlock.Text = "IsOpen is now " + cfoSettings.IsOpen;
      }

      Delete
    3. Hi John

      As mentioned above IsOpenChanged is used to for both the purpose. Can we differentiate between Opening and closing events of Flyout?
      I want to save the data only on closing event of flyout. What is appropriate way to do this task?
      So Please help.

      Thanks in advance
      Reply

      Delete

Note: Only a member of this blog may post a comment.