WPF — Floppy Pages

в 5:58, , рубрики: .net, C#, wpf, XAML, разработка под windows

Реализация нового Frame в стиле IOS

Или проще говоря — Frame в стиле Modern UI.

Здравствуйте. Меня зовут Андрей и я очень устал пользоваться стандартным VK на Windows 10. Его горизонтальная навигация меня утомила и как то она не вписывается в общий дизайн. Ещё очень давно хотел реализовать такое дело, а именно: плавная навигация как на iPhone. Для чего? Для того, что я хочу сделать свой VK клиент на WPF. Для начала покажу общую картину:

image

Можно сделать вывод, что такой подход будет очень удобным. DataContext между страницами будет передаваться через конструктор, но дальше будет интереснее.

Начну с namespace UFC.UI. Так как на каждой странице может находиться несколько кнопок, то мне пришлось создать интерфейс:

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

Каждая страница наследует этот интерфейс и получает очень удобное дополнение.

    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());
        }
    }

Теперь плавно можно подойти к интересному. Здесь затронут интерфейс IFloppyPages. Конечно его можно было бы по другому назвать, но я выбрал именно такое название. Его функция ни чем не отличается от DataContext. Такое решение сделано для того, что бы в будущем мы могли использовать DataContext в других целях (mvvm, binding, commands и т.д.)
Собственно, вот его реализация:

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; }
}

Пожалуй теперь можно взглянуть на xaml разметку этого элемента управления:

<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.LongPage.MarginAnimation">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.LongPage.MarginAnimation}"
                                        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.LongPage.MarginAnimation}"
                                        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.LongPage.MarginAnimation}">
                                         <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.LongPage.MarginAnimation}">
                                         <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>

Очень надеюсь, что вам удастся понять мой алгоритм. Всё самое непонятное постараюсь объяснить после кода внизу страницы.

Теперь приведу весь код этого элемента управления:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;

namespace UFC.UI
{
    /// <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 = "Страница по умолчанию";
        }
    }

    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; }
    }

    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; }
    }
}

namespace UFC.UI.Controls
{
    public class FloppyPages : Control, IFloppyPages, INotifyPropertyChanged
    {
        #region Private Members

        private bool GridNumber = false;
        private bool IsDoneAnimation = true;
        private List<IFloppyPage> journal = new List<IFloppyPage>();

        private Frame frame1 = new Frame();
        private Frame frame2 = new Frame();
        private Grid mainGrid = new Grid();
        private Grid grid1 = new Grid();
        private Grid grid2 = new Grid();

        private BeginStoryboard animation1 = new BeginStoryboard()
        { Storyboard = new Storyboard() };
        private BeginStoryboard animation2 = new BeginStoryboard()
        { Storyboard = new Storyboard() };
        private BeginStoryboard animation3 = new BeginStoryboard()
        { Storyboard = new Storyboard() };
        private BeginStoryboard animation4 = new BeginStoryboard()
        { Storyboard = new Storyboard() };

        #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()
        {
            FirstPage = new DefaultPage();
        }

        #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 { }
        }

        public int JournalCount
        {
            get
            {
                return journal.Count;
            }
            set { }
        }

        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 { }
        }

        #endregion

        #region Private OnFirstPage
        private void OnFirstPage(IFloppyPage page)
        {
            if (page != null && frame1 != null && frame2 != null)
            {
                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.LongPage.MarginAnimation"] =
                    new Thickness(this.ActualWidth, 0, -1 * this.ActualWidth, 0);
                };
            }

            OnFirstPage(FirstPage);
        }

        #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;
                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
    }
}

Начну с того, что ещё ранее вы могли заметить в xaml разметке этого элемента ресурс:

«Dynamic.LongPage.MarginAnimation».

И очень странно, почему там стояли такие размеры: 10,0,-10,0;

На самом деле это не так важно, потому что при сборке мы автоматически подписываемся на событие SizeChanged элемента mainGrid в методе OnApplyTemplate().

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

Благодаря такой реализации мы получаем элемент управления, где анимация двигается на то расстояние, на которое мы укажем, то есть просто изменив размер окна.

Напомню кстати говоря, что в методе OnApplyTemplate() мы получаем ссылки на все мелкие элементы из разметки методом GetTemplateChild(«mainGrid»);

Алгоритм получился таким: вы как бы видите одну страницу, потом при переходе на следующую страницу с правого края вылезает вторая страница. Первая страница уходит на задний план, затем после окончания анимации первая страница уходит в правый край где была вторая страница.

Таким образом мы получаем две чередующие панели, на которых лежат frame1 и frame2. Благодаря переменной GridNumber мы проверяем, на какой grid мы попали и на каком frame поменять страницу.

Так же здесь реализован журнал, но в нём ничего интересного нет. Обычный список, который удаляет IFloppyPage только после перехода «Назад» (GoBack).

Да и ещё. Как только приложение начинает свою жизнь, ему присваивается первая страница, это может быть либо DefaultPage по умолчанию, либо та страница, которую укажете вы. Затем FloppyPages автоматически привяжет ваш IFloppyPage к событию Navigate и GoBack. Так он будет следить, когда на одной из ваших IFloppyPage вы решитесь перейти на другую страницу.

Теперь покажу окно, где и создаётся FloppyPages, и присваивается первая страница.

using System.Windows;
using UFC.Pages;
namespace UFC
{
    public partial class Browser : Window
    {
        public Browser()
        {
            InitializeComponent();
            floppyPages.FirstPage = new MainPage()
            {
                IFloppyPages = floppyPages
            };
        }

        private void Button_GoBack(object sender, RoutedEventArgs e)
        {
            floppyPages.GoBack();
        }
    }
}

<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" >
    <Grid Background="LightGray">
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        
        <ufc:FloppyPages Grid.Row="1" Name="floppyPages" />

        <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 Z M 19,34L 27,34L 27,42L 19,42L 19,34 Z M 31,23L 57,23L 57,31L 31,31L 31,23 Z M 19,45L 27,45L 27,53L 19,53L 19,45 Z M 31,34L 57,34L 57,42L 31,42L 31,34 Z M 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 Z M 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 Z M 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>

Такое проектное решение без всякого труда позволит вам добавить и ViewModel, и Model и возможность использовать один и тот же DataContext на разных страницах.

Спасибо за внимание.

Автор: AdlibBeats

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js