티스토리 뷰

반응형

2022.09.06 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내v1.0 part9-2 StyleSelector

2022.08.31 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 v1.0 part9-1 DataTemplateSelector

2022.08.08 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 v1.0 part8-3 Template

2022.08.02 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 v1.0 part8-2 Template

2022.07.21 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 v1.0 part8-1 Template

2022.07.13 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 v1.0 part7 Behavior

2022.07.05 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 V1.0 part6 Command

2022.06.27 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 V1.0 part5 Converter

2022.06.15 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 V1.0 part4-2 Data Binding

2022.06.13 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 V1.0 part4-1 Data Binding

2022.06.07 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 V1.0 part3-3

2022.06.02 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 V1.0 part3-2

2022.05.30 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 V1.0 part3-1

2022.05.11 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 v1.0 part2

2022.05.09 - [WPF .NET] - MVVM Pattern을 사용하는 개발자를 위한 안내 v1.0 part1

Behavior

1. 특징

  • 컨트롤에 연결해서 사용해야 합니다.
  • 컨트롤을 수정하지 않고 컨트롤에 추가 기능을 구현 할 수 있습니다.
  • OnAttached(), OnDetaching() 메서드를 이용해서, 컨트롤에 연결될 때와 분리될 때의 동작을 정의할 수 있습니다.
  • AssociatedObject 프로퍼티를 이용해서 연결된 컨트롤에 접근 할 수 있습니다.
  • 컨트롤의 이벤트 핸들러를 이용해서 다양한 기능을 구현하고 사용할 수 있습니다.
  • 다른 화면이나 프로젝트에서도 사용할 수 있어 재사용성이 높습니다.
  • 대부분 XAML에서 사용되며, StaticResource로 만들 수 없습니다.

2. 왜 Behavior를 만들어서 사용해야하죠?

  • 기본 TextBox는 포커스를 받을 때 입력된 모든 택스트를 선택하는 기능이 없습니다.
  • 기능을 구현하기 위해서는 TextBox의 GotFocus 이벤트 핸들러를 추가하고, Dispatcher와 컨트롤의 SelectAll() 메서드를 이용해야 합니다.
    • 이런 기능들은 ViewModel에서 제어하기가 어렵고, 논리적 영역에서 컨트롤의 직접적인 제어를 하지 말아야 합니다. 그래서, Behavior에서 그런 기능을 담당하게 됩니다.
  • 모든 TextBox 컨트롤에 이 기능을 구현하려면 어떻게 하는 것이 좋을까요?
    • TextBox 컨트롤을 상속 받는 TextBoxEx라는 컨트롤을 만드는 방법
      • 컨트롤 상속을 이용해서 컨트롤을 생성하는 것은 복잡한 동작이 필요하거나, protected에 접근해야 하는 경우에만 제한적으로 사용하는 것이 권장됩니다.
    • Behavior를 만들어서 각 TextBox에 붙이는 방법
3rd party 상용 컨트롤을 이용하는 방법도 있습니다.~

3. Behavior 만들기

  • Microsoft.Xaml.Behaviors.Wpf nuget package를 추가합니다.
  • 클래스를 만듭니다.
  • Behavior<Control>을 상속 받습니다. Control은 Behavior가 사용하려는 컨트롤을 입력하시면 됩니다.
    • TextBox, CheckBox, ComboBox 등
  • OnAttached()와 OnDetaching() 메서드를 override 합니다.
    • OnAttached() 메서드에서 AssociatedObject 프로퍼티를 이용해서 사용하려는 이벤트 핸들러를 추가합니다.
    • OnDetaching() 메서드에서 연결했던 이벤트 핸들러를 제거합니다.
  • Dependency Property를 추가해서 Binding을 할 수도 있습니다.
  • 작성 완료 후 XAML에서 컨트롤에 붙여서 사용합니다.

TextBoxBehavior.cs

    public class TextBoxBehavior : Behavior<TextBox>
    {
        /// <summary>
        /// SelectAll 사용 여부 기본은 true
        /// </summary>
        public bool IsSelectAll { get; set; } = true;

        protected override void OnAttached()
        {
            //AssoicatedObject의 이벤트 핸들러 추가 - 삭제는 반드시 쌍으로 처리합니다.
            if (IsSelectAll)
            {
                AssociatedObject.GotFocus += AssociatedObject_GotFocus;
            }
            AssociatedObject.KeyDown += AssociatedObject_KeyDown;
        }

        private void AssociatedObject_KeyDown(object sender, KeyEventArgs e)
        {
            //Enter Key입력시 EnterCommand 실행 이벤트 아규먼트 넘기는 것은 옵션
            if (e.Key == Key.Enter && EnterCommand != null)
            {
                EnterCommand.Execute(e);
                e.Handled = true;
            }
        }

        private void AssociatedObject_GotFocus(object sender, System.Windows.RoutedEventArgs e)
        {
            //Dispatcher를 이용해야 기능이 정상 동작합니다.
            Dispatcher.BeginInvoke(
                () =>
                {
                    AssociatedObject.SelectAll();
                }, null);
        }

        protected override void OnDetaching()
        {
            if (IsSelectAll)
            {
                AssociatedObject.GotFocus -= AssociatedObject_GotFocus;
            }
            AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
        }

        public ICommand EnterCommand
        {
            get { return (ICommand)GetValue(EnterCommandProperty); }
            set { SetValue(EnterCommandProperty, value); }
        }

        /// <summary>
        /// EnterCommand
        /// </summary>
        public static readonly DependencyProperty EnterCommandProperty =
            DependencyProperty.Register(nameof(EnterCommand), typeof(ICommand), typeof(TextBoxBehavior), new PropertyMetadata(null));
    }

4. Behavior 사용하기

b 네임스페이스와 Behavior의 네임스페이스를 선언합니다.

Behavior를 붙이려는 컨트롤 내부에 아래와 같은 형태로 추가하시면 됩니다.

<Window
    x:Class="BehaviorSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:BehaviorSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
            <TextBox Text="{Binding Name}">
                <b:Interaction.Behaviors>
                    <local:TextBoxBehavior EnterCommand="{Binding EnterCommand}"/>
                </b:Interaction.Behaviors>
            </TextBox>

포커스시 전체 선택

엔터키를 입력했을 때

5. Behavior 만들 때 주의 사항

  • 디자인 타임에서도 생성되고 실행되기 때문에, 런타임에만 데이터가 등록되는 프로퍼티나 메서드의 사용시 주의 필요
  • 특히 Dependency Property를 사용하고, PropertyChanged 이벤트 핸들러를 추가한 경우 핸들러에서도 null 체크를 잘 해주어야 함
  • Loaded 이벤트를 사용하는 경우에도 디자인 타임에서 에러가 발생하지 않도록 주의해야 합니다.
  • DesignerProperties.GetIsInDesignMode(new DependencyObject()) 문구를 이용하면, 디자인 타임 여부를 확인할 수 있습니다.

아래와 같이 EnterCommandChanged 이벤트 핸들러를 추가해서 사용한다고 했을 때...

        /// <summary>
        /// EnterCommand
        /// </summary>
        public static readonly DependencyProperty EnterCommandProperty =
            DependencyProperty.Register(nameof(EnterCommand), typeof(ICommand), 
                typeof(TextBoxBehavior), new PropertyMetadata(null, EnterCommandChanged));

        private static void EnterCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (TextBoxBehavior)d;
            behavior.AssociatedObject.Text = "EnterCommandChanged";
        }

디자인 타임에 아래와 같은 오류가 발생합니다.

여기서 오류가 발생한 원인은 AssociatedObject가 null이기 때문입니다.

6. TextBox를 1000개도 넘게 사용하는데 일일이 추가해야 하나요?

  • Blend Behaviors in Styles
    • Style에서 Behavior 사용 여부를 지정할 수 있는 클래스입니다.
    • 사용법
      • public class TextBoxExBehavior : AttachableForStyleBehavior<TextBox, TextBoxExBehavior>
      • Behavior를 생성할 때 해당 클래스를 이용합니다.
      • 스타일을 만들고 아래와 같이 IsEnabledForStyle 속성을 true로 설정하고, 컨트롤에 스타일을 적용하면 Behavior가 자동으로 추가됩니다.

AttachableForStyleBehavior.cs

    /// <summary>
    /// 스타일에서 Behavior을 추가해서 사용할 수 있도록 지원합니다.
    /// </summary>
    /// <remarks>https://stackoverflow.com/questions/1647815/how-to-add-a-blend-behavior-in-a-style-setter</remarks>
    /// <typeparam name="TComponent"></typeparam>
    /// <typeparam name="TBehavior"></typeparam>
    public class AttachableForStyleBehavior<TComponent, TBehavior> : Behavior<TComponent>
            where TComponent : DependencyObject
            where TBehavior : AttachableForStyleBehavior<TComponent, TBehavior>, new()
    {
        #region IsEnabledForStyle

        public static DependencyProperty IsEnabledForStyleProperty =
            DependencyProperty.RegisterAttached(nameof(IsEnabledForStyle), typeof(bool),
            typeof(AttachableForStyleBehavior<TComponent, TBehavior>), new FrameworkPropertyMetadata(false, OnIsEnabledForStyleChanged));

        public bool IsEnabledForStyle
        {
            get => (bool)GetValue(IsEnabledForStyleProperty);
            set => SetValue(IsEnabledForStyleProperty, value);
        }

        private static void OnIsEnabledForStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            UIElement uie = d as UIElement;

            if (uie != null)
            {
                BehaviorCollection behColl = Interaction.GetBehaviors(uie);
                TBehavior existingBehavior = behColl.FirstOrDefault(b => b.GetType() ==
                      typeof(TBehavior)) as TBehavior;

                if ((bool)e.NewValue == false && existingBehavior != null)
                {
                    behColl.Remove(existingBehavior);
                }
                else if ((bool)e.NewValue == true && existingBehavior == null)
                {
                    behColl.Add(new TBehavior());
                }
            }
        }

        #endregion IsEnabledForStyle
    }

TextBoxExBehavior.cs

public class TextBoxExBehavior : AttachableForStyleBehavior<TextBox, TextBoxExBehavior>
{
    private readonly IDynamicResource _dr;

    public TextBoxExBehavior()
    {
        Debug.WriteLine("Create TextBoxExBehavior");
        if(App.IsInDesignMode())
        {
            return;
        }
        _dr = App.Current.Services.GetService(typeof(IDynamicResource)) as IDynamicResource;
    }

    protected override void OnAttached()
    {
        AssociatedObject.GotFocus += AssociatedObject_GotFocus;
        if (_dr == null) return;
        //OnAttached가 UI Thread에서 실행되지 않기 때문에 Dispatcher 이용해서 출력
        Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, 
            () => 
            {
                var text = _dr["wrd_Hello"];
                AssociatedObject.Text = text;
            });
    }

    private void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
    {
        Debug.WriteLine("AssociatedObject_GotFocus in TextBoxExBehavior");
    }

    protected override void OnDetaching()
    {
        AssociatedObject.GotFocus -= AssociatedObject_GotFocus;
    }
}

스타일 만들기

x:Key를 입력하지 않으면 모든 TextBox에 적용됩니다~
<Style x:Key="AttachedTextBoxExStyle" TargetType="TextBox">
    <Setter Property="FontSize" Value="20" />
    <Setter Property="local:TextBoxExBehavior.IsEnabledForStyle" Value="True" />
</Style>

스타일 적용

<TextBox Style="{StaticResource AttachedTextBoxExStyle}" Text="{Binding Address}" />

실행 화면

7. 마무리

Behavior에 대한 문의 사항은 언제나 환영합니다~

WpfTest/BehaviorSample at master · kaki104/WpfTest (github.com)

 

GitHub - kaki104/WpfTest

Contribute to kaki104/WpfTest development by creating an account on GitHub.

github.com

 

반응형
댓글