Maxime FRAPPAT

Hum …no thanks ! – Lordinaire

Category: UWP (Page 1 of 2)

[UWP] FlyoutView control

In a project, I needed a control like the old SettingsFlyout which is now obselete. What I wanted is just a panel that come-and-go with a great animation.

Style


<Style TargetType="flyoutView:FlyoutView">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="flyoutView:FlyoutView">
                <Grid>
                    <Grid x:Name="PART_OVERLAY" Background="{TemplateBinding BackgroundOverlay}" Visibility="Collapsed" />

                    <Grid x:Name="PART_CONTENT">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition />
                        </Grid.RowDefinitions>

                        <Grid.RenderTransform>
                            <CompositeTransform />
                        </Grid.RenderTransform>

                        <ContentControl ContentTemplate="{TemplateBinding HeaderTemplate}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" />

                        <ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The logic

We need to create the storyboards by hand because the content width/height aren’t static.

[TemplatePart(Name = PartOverlay, Type = typeof(Grid))]
[TemplatePart(Name = PartContent, Type = typeof(Grid))]
public partial class FlyoutView : ContentControl
{
    #region Dependency properties

    public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register(
            "HeaderTemplate", typeof(DataTemplate), typeof(FlyoutView),
            new PropertyMetadata(default(DataTemplate)));

    public DataTemplate HeaderTemplate
    {
        get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
        set { SetValue(HeaderTemplateProperty, value); }
    }

    public static readonly DependencyProperty PlacementProperty = DependencyProperty.Register(
        "Placement", typeof(PlacementType), typeof(FlyoutView),
        new PropertyMetadata(default(PlacementType), OnPlacementChanged));

    public PlacementType Placement
    {
        get { return (PlacementType)GetValue(PlacementProperty); }
        set { SetValue(PlacementProperty, value); }
    }

    public static readonly DependencyProperty UseDismissOvelayProperty = DependencyProperty.Register(
        "UseDismissOvelay", typeof(bool), typeof(FlyoutView),
        new PropertyMetadata(default(bool)));

    public bool UseDismissOvelay
    {
        get { return (bool)GetValue(UseDismissOvelayProperty); }
        set { SetValue(UseDismissOvelayProperty, value); }
    }

    public static readonly DependencyProperty BackgroundOverlayProperty = DependencyProperty.Register(
        "BackgroundOverlay", typeof(SolidColorBrush), typeof(FlyoutView),
        new PropertyMetadata(default(SolidColorBrush)));

    public SolidColorBrush BackgroundOverlay
    {
        get { return (SolidColorBrush)GetValue(BackgroundOverlayProperty); }
        set { SetValue(BackgroundOverlayProperty, value); }
    }

    public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register(
            "IsOpen", typeof(bool), typeof(FlyoutView),
            new PropertyMetadata(default(bool), OnIsOpenChanged));

    public bool IsOpen
    {
        get { return (bool)GetValue(IsOpenProperty); }
        set { SetValue(IsOpenProperty, value); }
    }

    public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(
        "ContentWidth", typeof(double), typeof(FlyoutView),
        new PropertyMetadata(default(double)));

    public double ContentWidth
    {
        get { return (double)GetValue(ContentWidthProperty); }
        set { SetValue(ContentWidthProperty, value); }
    }

    public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(
        "ContentHeight", typeof(double), typeof(FlyoutView),
        new PropertyMetadata(default(double)));

    public double ContentHeight
    {
        get { return (double)GetValue(ContentHeightProperty); }
        set { SetValue(ContentHeightProperty, value); }
    }

    #endregion

    #region Properties

    public enum PlacementType
    {

        Left,
        Top,
        Right,
        Bottom
    }

    private const string PartOverlay = "PART_OVERLAY";
    private const string PartContent = "PART_CONTENT";

    private Storyboard _openStoryboard;
    private Storyboard _closeStoryboard;
    private Grid _content;
    private Grid _overlay;

    #endregion

    #region Constructor

    public FlyoutView()
    {
        DefaultStyleKey = typeof(FlyoutView);
    }

    #endregion

    #region Override

    protected override void OnApplyTemplate()
    {
        _overlay = GetTemplateChild(PartOverlay) as Grid;
        _content = GetTemplateChild(PartContent) as Grid;

        if (_content != null)
        {
            _content.Loaded += OnContentLoaded;
            HandleOpening();
        }

        if (_overlay != null)
            _overlay.Tapped += OnOverlayTapped;

        UpdateFromLayout();

        base.OnApplyTemplate();
    }

    #endregion

    #region Methods

    private void UpdateFromLayout()
    {
        if (_content == null || _overlay == null)
            return;

        double overflow;
        if (Placement == PlacementType.Left
            || Placement == PlacementType.Right)
            overflow = Placement == PlacementType.Left
                ? -ContentWidth
                : ContentWidth;
        else
            overflow = Placement == PlacementType.Top
                ? -ContentHeight
                : ContentHeight;

        UpdatePlacement(overflow);
        UpdateAnimations(overflow);
    }

    private void UpdatePlacement(double overflow)
    {
        if (Placement == PlacementType.Left
            || Placement == PlacementType.Right)
        {
            _content.Width = ContentWidth;
            _content.Height = double.NaN;
            _content.RenderTransform = new CompositeTransform { TranslateX = overflow, TranslateY = 0 };
            _content.HorizontalAlignment = Placement == PlacementType.Left
                ? HorizontalAlignment.Left
                : HorizontalAlignment.Right;
            _content.VerticalAlignment = VerticalAlignment.Stretch;
        }
        else
        {
            _content.Width = double.NaN;
            _content.Height = ContentHeight;
            _content.RenderTransform = new CompositeTransform { TranslateX = 0, TranslateY = overflow };
            _content.VerticalAlignment = Placement == PlacementType.Top
                ? VerticalAlignment.Top
                : VerticalAlignment.Bottom;
            _content.HorizontalAlignment = HorizontalAlignment.Stretch;
        }
    }

    private void UpdateAnimations(double overflow)
    {
        var targetProperty = Placement == PlacementType.Left || Placement == PlacementType.Right
            ? "(UIElement.RenderTransform).(CompositeTransform.TranslateX)"
            : "(UIElement.RenderTransform).(CompositeTransform.TranslateY)";

        // Create animations
        _openStoryboard = new Storyboard();
        var openAnimation = new DoubleAnimation
        {
            Duration = TimeSpan.FromMilliseconds(500),
            From = overflow,
            To = 0,
            EasingFunction = new PowerEase { EasingMode = EasingMode.EaseIn, Power = 2 }
        };
        Storyboard.SetTargetProperty(openAnimation, targetProperty);
        Storyboard.SetTarget(openAnimation, _content);
        _openStoryboard.Children.Add(openAnimation);

        _closeStoryboard = new Storyboard();
        var closeAnimation = new DoubleAnimation
        {
            Duration = TimeSpan.FromMilliseconds(500),
            From = 0,
            To = overflow,
            EasingFunction = new PowerEase { EasingMode = EasingMode.EaseOut, Power = 2 }
        };
        Storyboard.SetTargetProperty(closeAnimation, targetProperty);
        Storyboard.SetTarget(closeAnimation, _content);
        _closeStoryboard.Children.Add(closeAnimation);
    }

    #endregion

    #region Events

    private void OnContentLoaded(object sender, object o)
    {
        _content.Loaded -= OnContentLoaded;

        UpdateFromLayout();
    }

    private void OnOverlayTapped(object sender, TappedRoutedEventArgs e)
    {
        if (IsOpen)
            IsOpen = false;
    }

    private static void OnIsOpenChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var control = sender as FlyoutView;

        control?.HandleOpening();
    }

    private void HandleOpening()
    {
        if (IsOpen)
        {
            if (UseDismissOvelay)
                _overlay.Visibility = Visibility.Visible;

            _openStoryboard?.Begin();
        }
        else
        {
            if (UseDismissOvelay)
                _overlay.Visibility = Visibility.Collapsed;

            _closeStoryboard?.Begin();
        }
    }

    private static void OnPlacementChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var control = sender as FlyoutView;

        control?.UpdateFromLayout();
    }

    #endregion
}

Usage

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Image Width="300" Height="300" Source="ms-appx:///Assets/ToolkitLogo.png" />

    <flyoutView:FlyoutView IsOpen="{Binding Path=IsOpen.Value, Mode=TwoWay}" Placement="{Binding Path=Placement.Value, Mode=TwoWay}" UseDismissOvelay="True" BackgroundOverlay="#BBF0F0F0" ContentWidth="300" ContentHeight="150">
        <flyoutView:FlyoutView.HeaderTemplate>
            <DataTemplate>
                <Grid Background="{StaticResource Brush-Grey-02}">
                    <TextBlock Text="{Binding Path=Header.Value}" Style="{StaticResource HeaderTextBlockStyle}" Margin="20" />
                </Grid>
            </DataTemplate>
        </flyoutView:FlyoutView.HeaderTemplate>

        <Grid Background="{StaticResource Brush-Grey-03}">
            <TextBlock HorizontalAlignment="Center" TextWrapping="Wrap" Text="This is the content" VerticalAlignment="Center" Style="{StaticResource SubtitleTextBlockStyle}" />
        </Grid>
    </flyoutView:FlyoutView>
</Grid>

Demo

[UWP] ReactiveUI & UWP Community Toolkit

Today, I try to make an UWP app with ReactiveUI and UWP Community Toolkit to do some amazing effects and animations. My main goal is to be able to add effects to my view without touching the code behind. To do that, I used a mix of behaviors, triggers and actions.

Reactive ViewModel

using ReactiveUI;
using System;
using System.Reactive;
using System.Threading.Tasks;
using UWPReactiveUI.Services.Impl;
using UWPReactiveUI.Services.Interfaces;
using UWPReactiveUI.Services.Models;

namespace UWPReactiveUI.Core
{
    public class HipsterViewModel : ReactiveObject
    {
        private ObservableAsPropertyHelper<HipsterSentence> _sentence;
        public HipsterSentence Sentence
        {
            get { return _sentence.Value; }
        }

        private ObservableAsPropertyHelper<bool> _isLoading;
        public bool IsLoading
        {
            get { return _isLoading.Value; }
        }

        public ReactiveCommand<Unit, HipsterSentence> ExecuteGetSentence { get; protected set; }

        public HipsterViewModel()
        {
            ExecuteGetSentence = ReactiveCommand.CreateFromTask(GetSentenceAsync);

            _isLoading = ExecuteGetSentence.IsExecuting.ToProperty(this, x => x.IsLoading, true);

            _sentence = ExecuteGetSentence.ToProperty(this, x => x.Sentence, new HipsterSentence { Text = "Hipster Eggs !" });
        }

        public async Task&lt;HipsterSentence&gt; GetSentenceAsync()
        {
            // We should use an IoC
            IHipsterService hipsterService = new HipsterService();

            // To be able to see the effects (for the demo)
            await Task.Delay(TimeSpan.FromSeconds(3));

            return await hipsterService.GetSentenceAsync();
        }
    }
}

The app does one thing (but do it well!) : when you execute the ExecuteGetSentence command, it returns some random ‘hipster’ sentences. It is based on this API : http://hipsterjesus.com/
If you are not very familiar with functional reactive programming, I highly recommend you to start with this introduction : https://github.com/reactiveui/ReactiveUI#introduction

Convert behavior to action

The UWP Community Toolkit give you a huge amount of eay-to-use effects (like blur, fade, scale, …) and that’s what I need here :) Let’s try to apply some blur effect on the UI when the command is launched.

The first solution is to call the effect in the code behind but I don’t want that, it hardly links the View with ViewModel :

myUIElement.Blur(value: 10, duration: 500, delay: 250);   

The second solution is to use a behavior :

<interactivity:Interaction.Behaviors>
    <behaviors:Blur Value="10" Duration="500" Delay="250" AutomaticallyStart="True" />
</interactivity:Interaction.Behaviors>

Well, it works fine but the effect is apply once and I can’t start it with some conditions. Let’s try to transform the Behavior to an Action. An Action can be call using a Trigger, a Trigger can be call using a Behavior.

Based on the behaviors source code, we can create two new classes :

CompositionActionBase.cs

using Microsoft.Toolkit.Uwp.UI.Animations.Behaviors;
using Microsoft.Xaml.Interactivity;
using Windows.UI.Xaml;

namespace UWPReactiveUI.Actions
{
    public abstract class CompositionActionBase : DependencyObject, IAction
    {
        /// <summary>
        /// The duration of the animation.
        /// </summary>
        public static readonly DependencyProperty DurationProperty = DependencyProperty.Register(nameof(Duration), typeof(double), typeof(CompositionBehaviorBase), new PropertyMetadata(1d));

        /// <summary>
        /// The delay of the animation.
        /// </summary>
        public static readonly DependencyProperty DelayProperty = DependencyProperty.Register(nameof(Delay), typeof(double), typeof(CompositionBehaviorBase), new PropertyMetadata(0d));

        /// <summary>
        /// Gets or sets the delay.
        /// </summary>
        /// <value>
        /// The delay.
        /// </value>
        public double Delay
        {
            get { return (double)GetValue(DelayProperty); }
            set { SetValue(DelayProperty, value); }
        }

        /// <summary>
        /// Gets or sets the duration.
        /// </summary>
        /// <value>
        /// The duration.
        /// </value>
        public double Duration
        {
            get { return (double)GetValue(DurationProperty); }
            set { SetValue(DurationProperty, value); }
        }

        public abstract object Execute(object sender, object parameter);
    }
}

Blur.cs

using Microsoft.Toolkit.Uwp.UI.Animations;
using Windows.UI.Xaml;

namespace UWPReactiveUI.Actions
{
    public class Blur : CompositionActionBase
    {
        /// <summary>
        /// The Opacity value of the associated object
        /// </summary>
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(double), typeof(Blur), new PropertyMetadata(1d));

        /// <summary>
        /// Gets or sets the Opacity.
        /// </summary>
        /// <value>
        /// The Opacity.
        /// </value>
        public double Value
        {
            get { return (double)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public override object Execute(object sender, object parameter)
        {
            var element = sender as FrameworkElement;
            if (element == null)
                return false;

            if (AnimationExtensions.IsBlurSupported)
            {
                element.Blur(duration: Duration, delay: Delay, value: (float)Value)?.Start();
            }
            return true;
        }
    }
}

Reactive View

We can now use a DataTriggerBehavior to execute the blur action if a condition is valid (IsLoading == true).

<Page x:Class="UWPReactiveUI.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:core="using:UWPReactiveUI.Core" xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:trigger="using:Microsoft.Xaml.Interactions.Core" xmlns:actions="using:UWPReactiveUI.Actions" mc:Ignorable="d">
    <Page.DataContext>
        <core:HipsterViewModel />
    </Page.DataContext>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Border BorderBrush="BurlyWood" BorderThickness="1" Margin="10">
            <Border>
                <interactivity:Interaction.Behaviors>
                    <trigger:DataTriggerBehavior Binding="{x:Bind ViewModel.IsLoading, Mode=OneWay}" Value="True">
                        <actions:Blur Value="3" Duration="1000" Delay="0" />
                    </trigger:DataTriggerBehavior>
                    <trigger:DataTriggerBehavior Binding="{x:Bind ViewModel.IsLoading, Mode=OneWay}" Value="False">
                        <actions:Blur Value="0" Duration="1000" Delay="0" />
                    </trigger:DataTriggerBehavior>
                </interactivity:Interaction.Behaviors>

                <TextBlock Text="{x:Bind ViewModel.Sentence.Text, Mode=OneWay}" TextWrapping="WrapWholeWords" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="3" />
            </Border>
        </Border>

        <ProgressRing VerticalAlignment="Center" HorizontalAlignment="Center" IsActive="True" Width="50" Height="50" Visibility="{x:Bind ViewModel.IsLoading, Converter={StaticResource BooleanToVisibilityConverter},Mode=OneWay}" />

        <Button Grid.Row="2" Margin="10" Command="{x:Bind ViewModel.ExecuteGetSentence, Mode=OneWay}" Content="GET SENTENCE" />

    </Grid>
</Page>

Another solution

You can also use the VisualStateManager to handle conditional states to call actions.

Source code

https://github.com/Lordinaire/UWP_ReactiveUI_Sample

 

Page 1 of 2

Powered by WordPress & Theme by Anders Norén

%d bloggers like this: