Sunday, November 3, 2013

Behaviors in WPF introduction

В данной статье постараюсь затронуть работу с Behaviors в WPF. Behaviors – это поведение представления в WPF. Поведение (Behavior) инкапсулирует часть функционального поведения в многократно используемом компоненте,  который в дальнейшем можно прикрепить к элементу в представлении. Это сделано для того, чтобы все преимущества паттерна MVVM оставить в первозданном виде, так как реализацию мы можем вынести в отдельный код и возложить всю работу на XAML, не нарушая принципов MVVM.  
На сегодняшний день выделяют два типа  Behaviors:
1 ) Attached Behavior представлен с помощью Attached Properties;
2) Blend Behavior  предназначен для добавления поведения для UIElement.
Рассмотрим более подробно эти представления.
Attached Behavior
В интернете все примеры по Attached Behavior, который многие программисты называют просто Attached Property, сводятся к написанию проверки на ввод в текстовое поле для того, чтобы разрешать или запрещать вводить в поле ввода (TextBox) только цифры. Примеры : Example1 и Example2. Проявим оригинальность и сделаем, например,  Attached Property  Close, с помощью которого при нажатии на клавишу escape будем закрывать форму. Вот как выглядит код реализации:
public static class WindowCloseBehaviour
    {
        public static void SetClose(DependencyObject target, bool value)
        {
            target.SetValue(CloseProperty, value);
        }
        public static readonly DependencyProperty CloseProperty =
                                                        DependencyProperty.RegisterAttached(
                                                        "Close",
                                                        typeof(bool),
                                                        typeof(WindowCloseBehaviour),
                                                        new UIPropertyMetadata(false, OnClose));

        private static void OnClose(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue is bool && ((bool)e.NewValue))
            {
                Window window = GetWindow(sender);
                if (window != null)
                {
                    window.KeyUp -= WindowKeyUp;
                    window.KeyUp += WindowKeyUp;
                };
            }
        }

        private static void WindowKeyUp(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Escape)
            {
                Window window = sender as Window;
                if (window != null)
                    window.Close();
            }
        }

        private static Window GetWindow(DependencyObject sender)
        {
            Window window = null;
            if (sender is Window)
                window = (Window)sender;
            if (window == null)
                window = Window.GetWindow(sender);
            return window;
        }
    }
 Использование в XAML будет выглядеть вот так.
<Window x:Class="WpfApplicationMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Behaviors="clr-namespace:Behaviors"
        Title="Library" Height="350" Width="525"
        Behaviors:WindowCloseBehaviour.Close="True">
    <Grid>
    </Grid>
</Window>
После запуска приложения и нажатия на клавиатуре клавиши Escape форма закроется.
Blend Behavior
Blend Behavior наследуется от класса Behavior<T> и доступно в пространстве System.Windows.Interactivity с Blend SDK . Но обычно этот SDK входит в поставку Visual Studio 2010/2012 и его можно найти при добавлении ссылки на библиотеку через Refference -> Add Refference…->Extensions. Для примера напишем простенький пример с использованием класса Behavior<T>, чтобы отловить потерю фокуса в контроле и вывести введенный текст в MessageBox. Суть примера - показать, что суть этого поведения - это вынесение логики с представления. Добавляем пространство имен:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
<TextBox>
    <i:Interaction.Behaviors>
        <Behaviors:LostFocusBehavior/>
    </i:Interaction.Behaviors>
</TextBox>
EventToCommand
Иногда нужен декоратор, чтобы представить event как command. Для этого существует решение преобразования с красноречивым названием EventToCommand. Этот решение доступно при использовании MVVM Light Toolkit , но если вы используете классическую модель MVVM,  это решение может вам пригодиться.
/// <summary>
    /// This <see cref="System.Windows.Interactivity.TriggerAction" /> can be
    /// used to bind any event on any FrameworkElement to an <see cref="ICommand" />.
    /// Typically, this element is used in XAML to connect the attached element
    /// to a command located in a ViewModel. This trigger can only be attached
    /// to a FrameworkElement or a class deriving from FrameworkElement.
    /// </summary>
    public class EventToCommand : TriggerAction<FrameworkElement>, ICommandSource
    {


        /// <summary>
        /// EventArgs
        /// </summary>
        public static readonly DependencyProperty EventArgsProperty = DependencyProperty.Register(
            "EventArgs",
            typeof(object),
            typeof(EventToCommand));


        /// <summary>
        /// Identifies the <see cref="CommandParameter" /> dependency property
        /// </summary>
        public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
            "CommandParameter",
            typeof(object),
            typeof(EventToCommand),
            new PropertyMetadata(
                null,
                (s, e) =>
                {
                    var sender = s as EventToCommand;
                    if (sender == null)
                    {
                        return;
                    }

                    if (sender.AssociatedObject == null)
                    {
                        return;
                    }

                    sender.EnableDisableElement();
                }));

        /// <summary>
        /// Identifies the <see cref="Command" /> dependency property
        /// </summary>
        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
            "Command",
            typeof(ICommand),
            typeof(EventToCommand),
            new PropertyMetadata(
                null,
                (s, e) => OnCommandChanged(s as EventToCommand, e)));

        /// <summary>
        /// Identifies the <see cref="MustToggleIsEnabled" /> dependency property
        /// </summary>
        public static readonly DependencyProperty MustToggleIsEnabledProperty = DependencyProperty.Register(
            "MustToggleIsEnabled",
            typeof(bool),
            typeof(EventToCommand),
            new PropertyMetadata(
                false,
                (s, e) =>
                {
                    var sender = s as EventToCommand;
                    if (sender == null)
                    {
                        return;
                    }

                    if (sender.AssociatedObject == null)
                    {
                        return;
                    }

                    sender.EnableDisableElement();
                }));

        /// <summary>
        /// Gets or sets the ICommand that this trigger is bound to. This
        /// is a DependencyProperty.
        /// </summary>
        public ICommand Command
        {
            get
            {
                return (ICommand)GetValue(CommandProperty);
            }

            set
            {
                SetValue(CommandProperty, value);
            }
        }

        /// <summary>
        /// Allows access to event args in binding
        /// <example>
        /// CommandParameter="{Binding EventArgs, RelativeSource={RelativeSource Self}}"
        /// </example>
        /// </summary>
        public object EventArgs
        {
            get
            {
                return GetValue(EventArgsProperty);
            }

            set
            {
                SetValue(EventArgsProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets an object that will be passed to the <see cref="Command" />
        /// attached to this trigger. This is a DependencyProperty.
        /// </summary>
        public object CommandParameter
        {
            get
            {
                return this.GetValue(CommandParameterProperty);
            }

            set
            {
                SetValue(CommandParameterProperty, value);
            }
        }

        public IInputElement CommandTarget
        {
            get { return this.AssociatedObject; }
        }


        /// <summary>
        /// Gets or sets a value indicating whether the attached element must be
        /// disabled when the <see cref="Command" /> property's CanExecuteChanged
        /// event fires. If this property is true, and the command's CanExecute
        /// method returns false, the element will be disabled. If this property
        /// is false, the element will not be disabled when the command's
        /// CanExecute method changes. This is a DependencyProperty.
        /// </summary>
        public bool MustToggleIsEnabled
        {
            get
            {
                return (bool)this.GetValue(MustToggleIsEnabledProperty);
            }

            set
            {
                SetValue(MustToggleIsEnabledProperty, value);
            }
        }


        /// <summary>
        /// Called when this trigger is attached to a FrameworkElement.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            EnableDisableElement();
        }


        /// <summary>
        /// Provides a simple way to invoke this trigger programatically
        /// without any EventArgs.
        /// </summary>
        public void Invoke()
        {
            Invoke(null);
        }

        /// <summary>
        /// Executes the trigger.
        /// <para>To access the EventArgs of the fired event, use a RelayCommand&lt;EventArgs&gt;
        /// and leave the CommandParameter and CommandParameterValue empty!</para>
        /// </summary>
        /// <param name="parameter">The EventArgs of the fired event.</param>
        protected override void Invoke(object parameter)
        {

            EventArgs = parameter;

            if (AssociatedElementIsDisabled())
            {
                return;
            }

            if (CanExecute())
                Execute();
        }

        private static void OnCommandChanged(
            EventToCommand element,
            DependencyPropertyChangedEventArgs e)
        {
            if (element == null)
            {
                return;
            }

            if (e.OldValue != null)
            {
                ((ICommand)e.OldValue).CanExecuteChanged -= element.OnCommandCanExecuteChanged;
            }

            var command = (ICommand)e.NewValue;

            if (command != null)
            {
                command.CanExecuteChanged += element.OnCommandCanExecuteChanged;
            }

            element.EnableDisableElement();
        }

        private bool AssociatedElementIsDisabled()
        {

            return AssociatedObject != null && !AssociatedObject.IsEnabled;
        }

        private void EnableDisableElement()
        {

            if (AssociatedObject == null)
            {
                return;
            }


            if (MustToggleIsEnabled && Command != null)
            {
                AssociatedObject.IsEnabled = CanExecute();
            }
        }

        private void OnCommandCanExecuteChanged(object sender, EventArgs e)
        {
            EnableDisableElement();
        }

        public void Execute()
        {
            if (Command == null)
                return;

            var routedCommand = Command as RoutedCommand;
            if (routedCommand != null)
                routedCommand.Execute(CommandParameter, CommandTarget);
            else
                Command.Execute(CommandParameter);
        }

        public bool CanExecute()
        {
            if (Command == null)
                return false;

            var routedCommand = Command as RoutedCommand;
            if (routedCommand != null)
                return routedCommand.CanExecute(CommandParameter, CommandTarget);

            return Command.CanExecute(CommandParameter);
        }
    }
Пример использования:
<Button Content="Add new book" Margin="3"  >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" >
            <Behaviors:EventToCommand
                Command="{Binding AddBookCommand}"
            />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
Итоги:

Суть поведений в WPF в том, чтобы поддерживать строгость паттерна MVVM и сократить код в представлении. Поведения особенно полезны при повторном использовании кода. Детальнее со всем вы можете ознакомиться в MSDN

No comments:

Post a Comment