티스토리 뷰

반응형

2023.02.06 - [WPF .NET] - Microsoft.Xaml.Behaviors.Wpf를 사용하기 Part1

 

Microsoft.Xaml.Behaviors.Wpf에서 가장 많이 사용되는 Action들에 대해서 알아보도록 하겠습니다.

Action들은 Trigger를 이용해서 특정 작업을 쉽게 처리하도록 설계되어 있습니다.
XAML에서 ViewModel의 값을 직접 변경도 가능하나, 이름이 틀리면 오류가 발생하고, Binding으로 충분히 처리가 가능하기 때문에, 사용을 권장하지 않습니다.

1. CallMethodAction

  • CallMethodAction은 트리거가 발생했을 때 TargetObject의 메서드를 호출합니다.
  • TargetObject는 Control이거나 ViewModel이 될 수 있습니다.
  • 기본 사용 방법 예제
<Button Content="SelectAll">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Click">
            <!--  PeopleListBox의 SelectAll() method 직접 호출  -->
            <b:CallMethodAction MethodName="SelectAll" TargetObject="{Binding ElementName=PeopleListBox}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
</Button>
<Button Content="UnselectAll">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Click">
            <!--  PeopleListBox의 UnselectAll() method 직접 호출  -->
            <b:CallMethodAction MethodName="UnselectAll" TargetObject="{Binding ElementName=PeopleListBox}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
</Button>
<Button Content="Add">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Click">
            <!--  뷰모델의 OnAdd public method 직접 호출  -->
            <b:CallMethodAction MethodName="OnAdd" TargetObject="{Binding}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
</Button>
  • 사용하는 경우
    • 컨트롤의 메서드를 호출하는 경우 주로 사용됩니다.
    • 뷰모델의 메서드를 직접 호출하는 것은 권장하지 않습니다. 메서드의 이름이 변경되는 경우 에러가 발생합니다.
  • 단점
    • Arguement를 넘길 수 없습니다. 즉 SelectAll(), UnselectAll(), OnAdd() 등의 메서드만 호출이 가능합니다.
    • TargetObject 자체 메서드는 호출이 가능하지만, 내부 프로퍼티의 메서드 호출은 불가합니다. 즉, PeopleListBox의 메서드는 호출가능하지만, PeopleListBox.Items의 메서드는 호출이 불가능 합니다.
ListBox의 SelectAll(), UnselectAll()를 실행하기 위해서는 ListBox의 SelectionModel가 Extended 혹은 Multiple이여야 합니다.

2. ChangePropertyAction

ChangePropertyAction은 호출 시 TargetObject의 특정 Property를 지정된 값으로 변경합니다.

  • <DataTrigger/>나 <Trigger/>의 <Setter/>와 같은 기능을 수행합니다.
  • PropertyName을 문자열로 입력하기 때문에 오탈자에 주의해야 합니다.
  • 아래 예제는 TextBlock에서 마우스 왼쪽 버튼을 클릭하면 Text 프로퍼티의 값을 '아이디'로 변경합니다.
<TextBlock
    x:Name="textBlock"
    Grid.Row="0"
    Text="Id">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="MouseLeftButtonDown">
            <b:ChangePropertyAction
                PropertyName="Text"
                TargetName="textBlock"
                Value="아이디" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
</TextBlock>

DataTemplate 내부에서 사용시 'Cannot set unknown member {http://schemas.microsoft.com/xaml/behaviors} Interaction.Triggers'.' Line number '6' and line position '14'.라는 XAML 오류가 발생합니다.

<DataTemplate x:Key="PersonItemTemplate">
    <Border x:Name="Bd" Background="Gray">
        <StackPanel Orientation="Horizontal">
            <TextBlock
                x:Name="Content"
                Margin="4"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                FontSize="12"
                Foreground="White"
                Style="{StaticResource AnimationTextBlockStyle}"
                Text="{Binding Name}"
                TextAlignment="Center" />
        </StackPanel>
        <b:Interaction.Triggers>
            <b:DataTrigger Binding="{Binding Sex}" Value="True">
                <b:ChangePropertyAction
                    PropertyName="Background"
                    TargetName="Bd"
                    Value="SkyBlue" />
                <b:ChangePropertyAction
                    PropertyName="Foreground"
                    TargetName="Content"
                    Value="DarkBlue" />
            </b:DataTrigger>
            <b:DataTrigger Binding="{Binding Sex}" Value="False">
                <b:ChangePropertyAction
                    PropertyName="Background"
                    TargetName="Bd"
                    Value="Pink" />
                <b:ChangePropertyAction
                    PropertyName="Foreground"
                    TargetName="Content"
                    Value="Blue" />
            </b:DataTrigger>
        </b:Interaction.Triggers>
    </Border>
</DataTemplate>
  • 단점
    • DataTrigger의 경우 마지막 상태를 계속 유지합니다. 여기서는 Value가 null인 경우에 기본 스타일로 변경이 되지 않습니다.
    • 이름이 정확하지 않는 경우 오류가 발생합니다.

3. GoToStateAction

GoToStateAction은 트리거될 때 FrameworkElement를 지정된 VisualState로 전환하는데 사용됩니다.

UWP 컨트롤들은 VisualState를 여러개 가지고 있는데, WPF 컨트롤들은 대부분 Triggers를 이용해서 구현되어 있습니다. 그래서, 굳이 VisualState를 이용해서 컨트롤의 동작을 제어할 필요는 없는 것으로 판단됩니다.

4. InvokeCommandAction

InvokeCommandAction은 특정 이벤트가 발생할 때마다 ICommand를 실행 합니다.

  • Behavior에서 가장 많이 사용되는 Action입니다.
  • Button 이외의 컨트롤은 Command 프로퍼티가 대부분 없기 때문에 ViewModel의 Command를 호출 할 수 없습니다. 그래서, 컨트롤들의 이벤트가 발생할 때 Command를 실행 시키기 위해서 반드시 사용되는 Action입니다.
  • 아래 예제는 ListBox의 SelectionChanged 이벤트가 발생할 때 SelectionChangedCommand를 실행합니다.
<ListBox
    x:Name="PeopleListBox"
    Grid.Row="1"
    ItemTemplate="{DynamicResource PersonItemTemplate}"
    ItemsSource="{Binding People}"
    SelectedItem="{Binding SelectedPerson, Mode=TwoWay}"
    SelectionMode="Extended">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="SelectionChanged">
            <b:InvokeCommandAction Command="{Binding SelectionChangedCommand}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
</ListBox>
/// <summary>
/// ListBox SelectionChanged Event가 발생할 때 실행되는 커맨드
/// </summary>
public ICommand SelectionChangedCommand { get; set; }

public MainWindowViewModel()
{
    //...
    
    RemoveCommand = new DelegateCommand(OnRemove);
    SelectionChangedCommand = new DelegateCommand(OnSelectionChanged);
}

/// <summary>
/// SelectionChanged 커맨드 실행
/// </summary>
private void OnSelectionChanged()
{
    Debug.WriteLine($"OnSelectionChanged : {SelectedPerson.Name}");
}
  • 예제는 DataGrid에서 이벤트가 발생할 때 뷰모델로 모델을 넘기는 방법에 대해 설명합니다.
    • 이 방법은 동일한 Row, Cell을 클릭할 때마다, Action을 처리해야 하는 경우 사용 할 수 있습니다.
    • EventArgs를 ViewModel에 깔끔하게 전달하는 방법에 대해서도 학습하실 수 있습니다.
  • Event가 발생하면, 이벤트와 함께 EventArgs가 발생합니다.
  • PreviewMouseLeftButtonUp 이벤트는 MouseButtonEventArgs가 발생합니다.
  • ViewModel에 EventArgs를 전달하는 방법은 2가지 입니다.
    • InvokeCommandAction에 PassEventArgsToCommand="True"를 이용하는 방법
      • 장점 : 사용하기 쉽습니다. 초보 사용자에게 권장
      • 단점 : EventArgs가 통으로 넘어가기 때문에 Type 변환을 위한 네임스페이스 추가가 필요하고, ViewModel에 View관련 코드가 추가되기 때문에 불편해 할 수 있습니다.
    • InvokeCommandAction에 EventArgsConverter를 이용하는 방법(Converter 추가 필요)
      • 장점 : ViewModel에서 바로 사용할 수 있는 Type으로 반환 가능, Converter이기 때문에 재사용 가능
      • 단점 : Type 변환을 위한 Converter를 하나 추가해야 하니 귀찮을 수 있습니다.
  • 첫번째 방법은 간단하니 2번째 방법을 사용해 보도록 하겠습니다.
/// <summary>
/// 이벤트 아규먼트를 뷰모델에서 사용하려는 데이터 형태로 가공하는 컨버터
/// </summary>
public class EventArgsToValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        //value를 EventArgs로 형변환 후 OriginalSource를 다시 FrameworkElement로 변환
        if(value is MouseButtonEventArgs args
            && args.OriginalSource is FrameworkElement element)
        {
            //컨트롤 내부 구성 요소가 여러가지 있는데 정확하게 원하는 컨트롤만 찍어서 반환해야 됩니다.
            //CheckBox가 그리드에 포함되어 있으면 CheckBox를 추가해야 합니다.
            switch(element)
            {
                case TextBlock:
                case DataGridCell:
                    return element.DataContext;
            }
        }
        //null리턴
        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

생성된 Converter를 Resources에 미리 등록합니다.

<converters:EventArgsToValueConverter x:Key="EventArgsToValueConverter" />

InvokeCommandAction에 EventArgsConverter를 연결합니다. 그러면, CommandParameter에 컨버터에서 반환된 값이 입력됩니다.

<DataGrid
    Grid.Row="7"
    Grid.Column="1"
    IsReadOnly="True"
    ItemsSource="{Binding Childs}">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="PreviewMouseLeftButtonUp">
            <b:InvokeCommandAction 
                Command="{Binding ElementName=RootGrid, Path=DataContext.DataGridCommand}"
                EventArgsConverter="{StaticResource EventArgsToValueConverter}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
</DataGrid>

나머지는 뷰모델에 작업합니다.

/// <summary>
/// DataGridCommand
/// </summary>
public ICommand DataGridCommand { get; set; }

public MainWindowViewModel()
{
    //...
    RemoveCommand = new DelegateCommand(OnRemove);
    SelectionChangedCommand = new DelegateCommand(OnSelectionChanged);
    DataGridCommand = new DelegateCommand<object>(OnDataGrid);
}

private void OnDataGrid(object obj)
{
    //obj가 null이면 종료
    if(obj is not Person person)
    {
        return;
    }
    Debug.WriteLine($"OnDataGrid : {person.Name}, Click count :{++_count}");
}

5. 실행

최종 소스는 다음 포스팅에서 공개 하도록 하겠습니다. 각 기능 하나마다 여러가지 테스트를 하다보니 시간이 좀 오래 걸리내요.. ㅜㅜ

 

반응형
댓글