[WinUI] ToggleSwitch에 Command 속성 추가

·

2 min read

UWP 및 WinUI 3의 [ToggleSwitch](https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.toggleswitch?view=winrt-22621) 컨트롤은 특이하게도 Command 속성이 없습니다. MVVM에서 사용하기 위해서는 CanExecute() 에 따라 컨트롤을 활성화/비활성화 할 필요가 있습니다.

Microsoft.Xaml.Interactivity.Behavior를 이용해서 다음 처럼 사용하고 싶은데요

<ToggleSwitch IsOn="{x:Bind SelectedAttribute.IsKey, Mode=OneWay}">
    <interactivity:Interaction.Behaviors>
        <extensions:ToggleSwitchCommandBehavior Command="{x:Bind TogglePKCommand}" />
    </interactivity:Interaction.Behaviors>
</ToggleSwitch>

ToggleSwitchCommandBehavior 는 다음처럼 구현할 수 있습니다.

using System.Windows.Input;

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.Xaml.Interactivity;

using Windows.System;

namespace WinUI.Extensions;

public class ToggleSwitchCommandBehavior : Behavior<ToggleSwitch>
{
    public readonly static DependencyProperty CommandProperty =
        DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(ToggleSwitchCommandBehavior),
            new PropertyMetadata(null, OnIsExternalChanged));

    public readonly static DependencyProperty CommandParameterProperty =
        DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(ToggleSwitchCommandBehavior),
            new PropertyMetadata(null));

    private bool _isAccessUserInputProperty;

    public ICommand? Command
    {
        get => (ICommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }

    public object? CommandParameter
    {
        get => GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }


    protected override void OnAttached()
    {
        AssociatedObject.PointerReleased += ToggleSwitch_PointerReleased;
        AssociatedObject.PreviewKeyDown += ToggleSwitch_PreviewKeyDown;
        AssociatedObject.Toggled += ToggleSwitch_Toggled;

        if (Command is not null)
        {
            Command_CanExecuteChanged(this, EventArgs.Empty);
        }
    }

    protected override void OnDetaching()
    {
        AssociatedObject.PointerReleased -= ToggleSwitch_PointerReleased;
        AssociatedObject.PreviewKeyDown -= ToggleSwitch_PreviewKeyDown;
        AssociatedObject.Toggled -= ToggleSwitch_Toggled;
    }

    private static void OnIsExternalChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
        if (sender is not ToggleSwitchCommandBehavior @this)
            return;

        if (args.OldValue is ICommand oldCommand)
            oldCommand.CanExecuteChanged -= @this.Command_CanExecuteChanged;

        if (args.NewValue is ICommand newCommand)
        {
            newCommand.CanExecuteChanged += @this.Command_CanExecuteChanged;
            if (@this.AssociatedObject is not null)
                @this.Command_CanExecuteChanged(@this, EventArgs.Empty);
        }
    }

    private void Command_CanExecuteChanged(object? sender, EventArgs e)
    {
        if (Command is null)
            return;

        AssociatedObject.IsEnabled = Command.CanExecute(CommandParameter);
    }

    private void ToggleSwitch_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
    {
        if (e.Key is VirtualKey.Space)
        {
            _isAccessUserInputProperty = true;
        }
    }

    private void ToggleSwitch_PointerReleased(object sender, PointerRoutedEventArgs e)
    {
        _isAccessUserInputProperty = true;
    }

    private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
    {
        var toggleSwitch = (ToggleSwitch)sender;

        // 사용자 입력일 때만 처리하도록 한다.
        if (_isAccessUserInputProperty is false)
            return;
        _isAccessUserInputProperty = false;

        if (Command is null)
            return;

        // 커멘드에 의해 값이 변하도록 IsOn 값을 원복한다.
        toggleSwitch.IsOn = !toggleSwitch.IsOn;

        if (Command.CanExecute(CommandParameter))
            Command.Execute(CommandParameter);
    }
}

ToggleSwitch가 토글 되었을 때 사용자 입력에 위해서인지를 확인하기 위해서는 마우스 입력이나 키보드 입력을 감지해야 합니다.
특이한 점은 WinUI의 ToggleSwitchTapped이벤트가 발생하지 않는 것인데요, 다행히 PointerReleased이벤트는 발생하여 이것을 이용하였습니다.