⬆️ ⬇️

WPF - Floppy Pages

Implementing a new frame in the style of IOS



Or simply put - Frame in the style of Modern UI.



Hello. My name is Andrew and I am very tired of using the standard VK on Windows 10. His horizontal navigation tired me and somehow it does not fit into the overall design. A long time ago I wanted to implement such a thing, namely: smooth navigation like on the iPhone. For what? For what I want to make my VK client on WPF. To begin, show the big picture:

image


It can be concluded that this approach will be very convenient. The DataContext between pages will be passed through the constructor, but it will be more interesting later.



I'll start with the namespace UFC.UI. Since there may be several buttons on each page, I had to create an interface:

')



interface IFloppyPage
public delegate void FloppyPageNavigateEventHandler(IFloppyPage page, FloppyPageEventArgs e); public delegate void FloppyPageGoBackEventHandler(FloppyPageEventArgs e); public class FloppyPageEventArgs : EventArgs { public FloppyPageEventArgs() { } } public interface IFloppyPage { event FloppyPageNavigateEventHandler Navigate; event FloppyPageGoBackEventHandler GoBack; IFloppyPages IFloppyPages { get; set; } string Title { get; set; } } 






image


Each page inherits this interface and gets a very convenient addition.



partial class Page1: Page, IFloppyPage
  public partial class Page1 : Page, IFloppyPage { public event FloppyPageNavigateEventHandler Navigate; public event FloppyPageGoBackEventHandler GoBack; public IFloppyPages IFloppyPages { get; set; } public Page1() : this(null) { } public Page1(object dataContext) { InitializeComponent(); if (dataContext != null) this.DataContext = dataContext; else this.DataContext = this; Title = " "; } private void NavigateTo_MainPage(object sender, RoutedEventArgs e) { if (Navigate != null) Navigate(new MainPage(DataContext), new FloppyPageEventArgs()); } private void NavigateTo_Page2(object sender, RoutedEventArgs e) { if (Navigate != null) Navigate(new Page2(DataContext), new FloppyPageEventArgs()); } private void NavigateTo_Page3(object sender, RoutedEventArgs e) { if (Navigate != null) Navigate(new Page3(DataContext), new FloppyPageEventArgs()); } private void Button_GoBack(object sender, RoutedEventArgs e) { if (GoBack != null) GoBack(new FloppyPageEventArgs()); } } 






Now you can smoothly go to the interesting. This is where the IFloppyPages interface is affected. Of course, it could have been called differently, but I chose that name. Its function is no different from the DataContext. This solution was made so that in the future we could use the DataContext for other purposes (mvvm, binding, commands, etc.)

Actually, here is its implementation:



interface IFloppyPages
 public interface IFloppyPages { IFloppyPage FirstPage { get; set; } IFloppyPage CurrentPage { get; set; } int JournalCount { get; set; } void Navigate(IFloppyPage page); bool GoBack(); bool CanGoBack { get; set; } } 






Perhaps now you can take a look at the xaml markup of this control:



local: FloppyPages
 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:UFC.UI.Controls"> <Thickness x:Key="Dynamic.ThicknessAnimation.Margin">10, 0, -10, 0</Thickness> <Style TargetType="local:FloppyPages"> <Style.Setters> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:FloppyPages"> <Grid Name="mainGrid"> <Grid Name="grid1"> <Frame Name="frame1" NavigationUIVisibility="Hidden"/> </Grid> <Grid Name="grid2"> <Frame Name="frame2" NavigationUIVisibility="Hidden"/> </Grid> <Grid.Resources> <BeginStoryboard x:Key="grid1Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="{DynamicResource Dynamic.ThicknessAnimation.Margin}" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="0" To="-100, 20, 100, 20"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> <BeginStoryboard x:Key="grid2Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="{DynamicResource Dynamic.ThicknessAnimation.Margin}" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="0" To="-100, 20, 100, 20"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> <BeginStoryboard x:Key="grid3Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="0" To="{DynamicResource Dynamic.ThicknessAnimation.Margin}"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="-100, 20, 100, 20" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> <BeginStoryboard x:Key="grid4Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="0" To="{DynamicResource Dynamic.ThicknessAnimation.Margin}"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="-100, 20, 100, 20" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> </Grid.Resources> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style.Setters> </Style> </ResourceDictionary> 






I really hope that you will be able to understand my algorithm. All the most incomprehensible try to explain after the code at the bottom of the page



Now I will give all the code for this control:



UFC.UI & UFC.UI.Controls
 using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Animation; namespace UFC.UI { #region DefaultPage /// <summary> ///   . /// </summary> internal class DefaultPage : IFloppyPage { public event FloppyPageNavigateEventHandler Navigate; public event FloppyPageGoBackEventHandler GoBack; public IFloppyPages IFloppyPages { get; set; } public string Title { get; set; } public DefaultPage() { Title = "  "; } } #endregion #region IFloppyPage public delegate void FloppyPageNavigateEventHandler(IFloppyPage page, FloppyPageEventArgs e); public delegate void FloppyPageGoBackEventHandler(FloppyPageEventArgs e); public class FloppyPageEventArgs : EventArgs { public FloppyPageEventArgs() { } } public interface IFloppyPage { event FloppyPageNavigateEventHandler Navigate; event FloppyPageGoBackEventHandler GoBack; IFloppyPages IFloppyPages { get; set; } string Title { get; set; } } #endregion #region IFloppyPages public interface IFloppyPages { IFloppyPage FirstPage { get; set; } IFloppyPage CurrentPage { get; set; } int JournalCount { get; set; } void Navigate(IFloppyPage page); bool GoBack(); bool CanGoBack { get; set; } } #endregion } namespace UFC.UI.Controls { #region FloppyPages public class FloppyPages : Control, IFloppyPages, INotifyPropertyChanged { #region Private Members private bool GridNumber = false; private bool IsDoneAnimation = true; private bool IsDoneInitialization = false; private List<IFloppyPage> journal = new List<IFloppyPage>(); private Frame frame1 = null; private Frame frame2 = null; private Grid mainGrid = null; private Grid grid1 = null; private Grid grid2 = null; private BeginStoryboard animation1 = null; private BeginStoryboard animation2 = null; private BeginStoryboard animation3 = null; private BeginStoryboard animation4 = null; #endregion #region Constructors static FloppyPages() { DefaultStyleKeyProperty.OverrideMetadata(typeof(FloppyPages), new FrameworkPropertyMetadata(typeof(FloppyPages))); FloppyPages.NavigatedRoutedEvent = EventManager.RegisterRoutedEvent("Navigated", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FloppyPages)); FloppyPages.WentBackRoutedEvent = EventManager.RegisterRoutedEvent("WentBack", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FloppyPages)); } public FloppyPages() { } #endregion #region Public Dependency Properties public static readonly DependencyProperty FirstPageProperty = DependencyProperty.RegisterAttached("FirstPage", typeof(IFloppyPage), typeof(FloppyPages)); #endregion #region Public Properties public IFloppyPage FirstPage { get { return (IFloppyPage)GetValue(FirstPageProperty); } set { SetValue(FirstPageProperty, value); OnFirstPage(FirstPage); OnPropertyChanged("FirstPage"); } } #endregion #region Public RoutedEvents public static readonly RoutedEvent NavigatedRoutedEvent; public static readonly RoutedEvent WentBackRoutedEvent; #endregion #region Public Events public event RoutedEventHandler Navigated { add { base.AddHandler(FloppyPages.NavigatedRoutedEvent, value); } remove { base.RemoveHandler(FloppyPages.NavigatedRoutedEvent, value); } } public event RoutedEventHandler WentBack { add { base.AddHandler(FloppyPages.WentBackRoutedEvent, value); } remove { base.RemoveHandler(FloppyPages.WentBackRoutedEvent, value); } } #endregion #region Public Members public IFloppyPage CurrentPage { get { if (journal.Count > 0) return journal[journal.Count - 1]; else return null; } set { /* Binding*/ } } public int JournalCount { get { return journal.Count; } set { /* Binding*/ } } public void Navigate(IFloppyPage page) { Start_Navigate(page); } public bool GoBack() { return Start_GoBack(); } public bool CanGoBack { get { if (journal.Count > 1) return true; else return false; } set { /* Binding*/ } } #endregion #region Private OnFirstPage private void OnFirstPage(IFloppyPage page) { if (page != null && IsDoneInitialization) { if (GridNumber) frame1.Navigate(page); else frame2.Navigate(page); page.Navigate += Page_Navigate; page.GoBack += Page_GoBack; journal.Clear(); journal.Add(page); OnPropertyChanged("JournalCount"); OnPropertyChanged("CanGoBack"); OnPropertyChanged("CurrentPage"); } } #endregion #region Public OnApplyTemplate public override void OnApplyTemplate() { base.OnApplyTemplate(); mainGrid = GetTemplateChild("mainGrid") as Grid; grid1 = GetTemplateChild("grid1") as Grid; if (grid1 != null) grid1.Margin = new Thickness(0); grid2 = GetTemplateChild("grid2") as Grid; if (grid2 != null) grid2.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); frame1 = GetTemplateChild("frame1") as Frame; frame2 = GetTemplateChild("frame2") as Frame; animation1 = mainGrid.Resources["grid1Animation"] as BeginStoryboard; animation2 = mainGrid.Resources["grid2Animation"] as BeginStoryboard; animation3 = mainGrid.Resources["grid3Animation"] as BeginStoryboard; animation4 = mainGrid.Resources["grid4Animation"] as BeginStoryboard; if (animation1 != null) if (animation1.Storyboard != null) animation1.Storyboard.Completed += NewGridMargin_Completed; if (animation2 != null) if (animation2.Storyboard != null) animation2.Storyboard.Completed += NewGridMargin_Completed; if (animation3 != null) if (animation3.Storyboard != null) animation3.Storyboard.Completed += OldGridMargin_Completed; if (animation4 != null) if (animation4.Storyboard != null) animation4.Storyboard.Completed += OldGridMargin_Completed; if (mainGrid != null) { mainGrid.SizeChanged += (sender, e) => { Application.Current.Resources["Dynamic.ThicknessAnimation.Margin"] = new Thickness(this.ActualWidth, 0, -1 * this.ActualWidth, 0); }; } IsDoneInitialization = true; FirstPage = new DefaultPage(); } #endregion #region Private Events private void Page_Navigate(IFloppyPage page, FloppyPageEventArgs e) { Start_Navigate(page); } private void Page_GoBack(FloppyPageEventArgs e) { Start_GoBack(); } private void NewGridMargin_Completed(object sender, EventArgs e) { Set_NewMargin(); } private void OldGridMargin_Completed(object sender, EventArgs e) { Set_OldMargin(); } #endregion #region Private Navigate private void Start_Navigate(IFloppyPage page) { if (page != null && IsDoneAnimation) { IsDoneAnimation = false; GridNumber = !GridNumber; page.Navigate += Page_Navigate; page.GoBack += Page_GoBack; if (!GridNumber) { animation1.Storyboard.Stop(); frame2.Navigate(page); Panel.SetZIndex(grid1, 0); Panel.SetZIndex(grid2, 1); grid2.Visibility = Visibility.Visible; animation2.Storyboard.Begin(); } else { animation2.Storyboard.Stop(); frame1.Navigate(page); Panel.SetZIndex(grid2, 0); Panel.SetZIndex(grid1, 1); grid1.Visibility = Visibility.Visible; animation1.Storyboard.Begin(); } journal.Add(page); OnPropertyChanged("JournalCount"); OnPropertyChanged("CurrentPage"); OnPropertyChanged("CanGoBack"); base.RaiseEvent(new RoutedEventArgs(FloppyPages.NavigatedRoutedEvent, this)); } } private void Set_NewMargin() { if (!GridNumber) { grid2.Margin = new Thickness(0); grid1.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid1.Visibility = Visibility.Hidden; } else { grid1.Margin = new Thickness(0); grid2.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid2.Visibility = Visibility.Hidden; } IsDoneAnimation = true; } #endregion #region Private GoBack private bool Start_GoBack() { if (journal.Count > 1 && IsDoneAnimation) { IsDoneAnimation = false; GridNumber = !GridNumber; journal[journal.Count - 1].Navigate -= Page_Navigate; journal[journal.Count - 1].GoBack -= Page_GoBack; grid1.Visibility = Visibility.Visible; grid2.Visibility = Visibility.Visible; if (!GridNumber) { animation4.Storyboard.Stop(); grid2.Margin = new Thickness(0); frame2.Navigate(journal[journal.Count - 2]); animation3.Storyboard.Begin(); } else { animation3.Storyboard.Stop(); grid1.Margin = new Thickness(0); frame1.Navigate(journal[journal.Count - 2]); animation4.Storyboard.Begin(); } journal.Remove(journal[journal.Count - 1]); OnPropertyChanged("JournalCount"); OnPropertyChanged("CurrentPage"); OnPropertyChanged("CanGoBack"); base.RaiseEvent(new RoutedEventArgs(FloppyPages.WentBackRoutedEvent, this)); return true; } else return false; } private void Set_OldMargin() { if (!GridNumber) { Panel.SetZIndex(grid1, 0); Panel.SetZIndex(grid2, 1); grid1.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid1.Visibility = Visibility.Hidden; } else { Panel.SetZIndex(grid1, 1); Panel.SetZIndex(grid2, 0); grid2.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid2.Visibility = Visibility.Hidden; } IsDoneAnimation = true; } #endregion #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } #endregion } #endregion } 






To begin with, you may have noticed a resource in the xaml markup of this element earlier:



"Dynamic.ThicknessAnimation.Margin".



And it is very strange why there were such sizes: 10.0, -10.0;



In fact, this is not so important, because when building, we automatically subscribe to the SizeChanged event of the mainGrid element in the OnApplyTemplate () method.



 if (mainGrid != null) { mainGrid.SizeChanged += (sender, e) => { Application.Current.Resources["Dynamic.ThicknessAnimation.Margin"] = new Thickness(this.ActualWidth, 0, -1 * this.ActualWidth, 0); }; } 


Thanks to this implementation, we get a control where the animation moves to the distance we specify, that is, simply by changing the window size.



Let me remind, incidentally, that in the OnApplyTemplate () method we get links to all the small elements from the markup using the GetTemplateChild method ("mainGrid");



The algorithm turned out like this: you kind of see one page, then when you go to the next page, the second page comes out from the right edge. The first page goes to the background, then after the end of the animation, the first page goes to the right edge where the second page was.



Thus we get two alternating panels, on which frame1 and frame2 lie. Thanks to the GridNumber variable, we check on which grid we hit and on which frame to change the page.



The magazine is also implemented here, but there is nothing interesting in it. A regular list that deletes IFloppyPage only after going to GoBack.



And more. As soon as the application starts its life, the first page is assigned to it, it can be either DefaultPage by default, or that page that you specify. Then, FloppyPages will automatically link your IFloppyPage to the Navigate and GoBack event. So it will follow when on one of your IFloppyPage you decide to go to another page.



Now I will show the window where FloppyPages is created, and the first page is assigned. I want to immediately warn you that I did not focus on the exterior design and the rounded buttons, but on the structure and its further use.



partial class Browser: Window
 using System.Windows; using UFC.Pages; namespace UFC { public partial class Browser : Window { public Browser() { InitializeComponent(); } private void floppyPages_Navigated(object sender, RoutedEventArgs e) { if (floppyPages.CanGoBack) BackButton.Visibility = Visibility.Visible; } private void floppyPages_WentBack(object sender, RoutedEventArgs e) { if (!floppyPages.CanGoBack) BackButton.Visibility = Visibility.Hidden; } private void Button_GoBack(object sender, RoutedEventArgs e) { floppyPages.GoBack(); } private void Window_Loaded(object sender, RoutedEventArgs e) { floppyPages.FirstPage = new MainPage(); } } } 






Window xClass UFC Browser
 <Window x:Class="UFC.Browser" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ufc="clr-namespace:UFC.UI.Controls;assembly=UFC.UI" Title="UFC" Height="640" Width="380" Loaded="Window_Loaded"> <Grid Background="LightGray"> <Grid.RowDefinitions> <RowDefinition Height="40"/> <RowDefinition/> </Grid.RowDefinitions> <ufc:FloppyPages Grid.Row="1" Name="floppyPages" Navigated="floppyPages_Navigated" WentBack="floppyPages_WentBack"/> <Grid Grid.Row="0" Background="LightGray"> <Grid.ColumnDefinitions> <ColumnDefinition Width="40"/> <ColumnDefinition Width="1*"/> <ColumnDefinition Width="40"/> </Grid.ColumnDefinitions> <TextBox Grid.Column="1" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" IsReadOnly="True" Background="Transparent" FontSize="20" Text="{Binding ElementName=floppyPages, Path=CurrentPage.Title, UpdateSourceTrigger=PropertyChanged}"/> <Button Name="MenuButton" Grid.Column="0" Visibility="Visible"> <Path Margin="5" Stretch="UniformToFill" Fill="Black" Data="F1 M 19,23L 27,23L 27,31L 19,31L 19,23 ZM 19,34L 27,34L 27,42L 19,42L 19,34 ZM 31,23L 57,23L 57,31L 31,31L 31,23 ZM 19,45L 27,45L 27,53L 19,53L 19,45 ZM 31,34L 57,34L 57,42L 31,42L 31,34 ZM 31,45L 57,45L 57,53L 31,53L 31,45 Z "/> </Button> <Button Name="BackButton" Grid.Column="0" Visibility="Hidden" Click="Button_GoBack"> <Path Margin="5,9" Stretch="UniformToFill" Fill="Black" Data="F1 M 18.0147,41.5355C 16.0621,39.5829 16.0621,36.4171 18.0147,34.4645L 26.9646,25.5149C 28.0683,24.4113 29,24 31,24L 52,24C 54.7614,24 57,26.2386 57,29L 57,47C 57,49.7614 54.7614,52 52,52L 31,52C 29,52 28.0683,51.589 26.9646,50.4854L 18.0147,41.5355 ZM 47.5281,42.9497L 42.5784,37.9999L 47.5281,33.0502L 44.9497,30.4717L 40,35.4215L 35.0502,30.4717L 32.4718,33.0502L 37.4215,37.9999L 32.4718,42.9497L 35.0502,45.5281L 40,40.5783L 44.9497,45.5281L 47.5281,42.9497 Z "/> </Button> <Button Grid.Column="2"> <Path Margin="5,9" Stretch="UniformToFill" Fill="Black" Data="F1 M 57.9853,41.5355L 49.0354,50.4854C 47.9317,51.589 47,52 45,52L 24,52C 21.2386,52 19,49.7614 19,47L 19,29C 19,26.2386 21.2386,24 24,24L 45,24C 47,24 47.9317,24.4113 49.0354,25.5149L 57.9853,34.4645C 59.9379,36.4171 59.9379,39.5829 57.9853,41.5355 ZM 28.4719,42.9497L 31.0503,45.5281L 36,40.5784L 40.9498,45.5281L 43.5282,42.9497L 38.5785,37.9999L 43.5282,33.0502L 40.9498,30.4718L 36,35.4215L 31.0503,30.4718L 28.4719,33.0502L 33.4216,37.9999L 28.4719,42.9497 Z "/> </Button> </Grid> </Grid> </Window> 






Such a design solution without any difficulty will allow you to add both the ViewModel and Model and the ability to use the same DataContext on different pages.







Thanks for attention.

Source: https://habr.com/ru/post/317896/



All Articles