MVVM방식으로 Textbox에 포커스를 주는 방법

이번에 다양한 버튼 클릭시 어떤 Textbox로 포커스를 가게하는 요구사항이 들어와 프로그램을 수정하려고 합니다.

각 버튼마다 Focus를 주는 방법도 있겠지만 MVVM방식으로 만든 프로그램이기도하고, 다양한 버튼에서 해당 포커스를 요구하기 때문에 각 버튼에 주면 코드가 더러워질 것 같아 MVVM방식으로 찾아봤습니다.

저는 위 방법을 통해 포커스를 주었는데, 혹시 다른분들은 어떻게 포커스를 주시는지 궁금하네요!
혹시 저랑 같은 고민을 하셨던 분이라면 저 유튜브가 도움이 될 것 같기도합니다!

4개의 좋아요

저같은 경우는 재사용을 위해서 Behavior를 사용합니다.
Behavior로 만들어놓으면

<behavior:Interaction.Behaviors>
      <local:FocusBehavior IsControlFocus="{Binding DataContext.IsUserNameFocus, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"/>
</behavior:Interaction.Behaviors>

<behavior:Interaction.Behaviors>
      <local:FocusBehavior IsControlFocus="{Binding DataContext.IsPasswordFocus, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"/>
</behavior:Interaction.Behaviors>

해당 부분만 복붙이 가능해서 편합니다.

FocusBehavior.cs

using Microsoft.Xaml.Behaviors;
using System.Windows;
using System.Windows.Controls.Primitives;

namespace WpfApp1
{
    public class FocusBehavior : Behavior<TextBoxBase>
    {
        public static readonly DependencyProperty IsControlFocusProperty =
            DependencyProperty.Register(nameof(IsControlFocus), typeof(bool), typeof(FocusBehavior), new PropertyMetadata(false, propertyChangedCallback: IsControlFocusPropertyChanged));

        public bool IsControlFocus
        {
            get { return (bool)GetValue(IsControlFocusProperty); }
            set { SetValue(IsControlFocusProperty, value); }
        }

        private static void IsControlFocusPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is FocusBehavior behavior)
            {
                behavior.AssociatedObject.Focus();
                behavior.AssociatedObject.SelectAll();
            }
        }
    }
}

MainWindow.xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors" 
        x:Class="WpfApp1.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="450">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>

    <StackPanel VerticalAlignment="Center">
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center">
            <StackPanel.Resources>
                <Style TargetType="TextBox">
                    <Setter Property="Width" Value="100"/>
                    <Setter Property="Margin" Value="0,5"/>
                </Style>
            </StackPanel.Resources>

            <TextBlock Text="UserName"/>
            <TextBox>
                <behavior:Interaction.Behaviors>
                    <local:FocusBehavior IsControlFocus="{Binding DataContext.IsUserNameFocus, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"/>
                </behavior:Interaction.Behaviors>
            </TextBox>

            <TextBlock Text="Password"/>
            <TextBox>
                <behavior:Interaction.Behaviors>
                    <local:FocusBehavior IsControlFocus="{Binding DataContext.IsPasswordFocus, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"/>
                </behavior:Interaction.Behaviors>
            </TextBox>

        </StackPanel>

        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center">
            <StackPanel.Resources>
                <Style TargetType="Button">
                    <Setter Property="Width" Value="120"/>
                    <Setter Property="Height" Value="30"/>
                    <Setter Property="Margin" Value="5,0"/>
                </Style>
            </StackPanel.Resources>

            <Button Content="UserName Focus"
                    Command="{Binding UserNameFocusCommand}"/>
            <Button Content="Password Focus"
                    Command="{Binding PasswordFocusCommand}"/>
        </StackPanel>
    </StackPanel>

</Window>

MainWindowViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace WpfApp1
{
    public partial class MainWindowViewModel : ObservableObject
    {
        [ObservableProperty]
        private bool _isUserNameFocus;

        [ObservableProperty]
        private bool _isPasswordFocus;

        public MainWindowViewModel()
        {
            
        }

        [RelayCommand]
        private void UserNameFocus()
        {
            IsUserNameFocus = !IsUserNameFocus;
        }

        [RelayCommand]
        private void PasswordFocus()
        {
            IsPasswordFocus = !IsPasswordFocus;
        }
    }
}

1
2

10개의 좋아요

포커스를 가져와야 하는 속성이 여러 개라면 동적으로 처리하는 게 편하지 않을까 싶어서 짜봤습니다.

  1. 포커스가 필요한 속성의 이름을 dummy TextBox에 바인딩
  2. dummy TextBox의 TextChanged 이벤트를 통해 포커스가 필요한 속성의 이름을 가져옴
  3. 자식 컨트롤 중에 포커스가 필요한 속성과 바인딩된 Dependency Property 또는 Attached Property가 있는 컨트롤이 있는지 확인 후 해당 컨트롤을 가져옴
  4. 해당 컨트롤이 FrameworkElement인지 확인 후, 맞으면 포커스 이동

단점

  1. 컨트롤을 가져오기 위해 논리적 트리를 순회하기 때문에 직접 가져오는 방법에 비해서 속도가 떨어질 수 있다.
  2. 뷰모델의 속성과 DP or AP간 일대일 바인딩일 때만 정상적으로 동작하며, 바인딩된 DP or AP가 여러 개일 때는 논리적 트리에서 가장 먼저 발견되는 컨트롤로 포커스가 이동한다.

MainViewModel.cs

public class MainViewModel : INotifyPropertyChanged
{
	// UserName, Password 등 생략 //
	public string? FocusTargetPropertyName
	{
		get => _focusTargetPropertyName;
		set
		{
			_focusTargetPropertyName = value;
			OnPropertyChanged();
		}
	}

	// Command 구현부 생략 //
	private void Login()
	{
		if (string.IsNullOrEmpty(UserName))
		{
			Message = "사용자 이름을 입력하세요";
			FocusTargetPropertyName = nameof(UserName);
			return;
		}
		else if (string.IsNullOrEmpty(Password))
		{
			Message = "패스워드를 입력하세요.";
			FocusTargetPropertyName = nameof(Password);
			return;
		}

		Message = "로그인 성공!";
	}
}

MainWindow.xaml

<Window x:Class="SetFocusInMvvm.MainWindow"
        <!-- xml 네임스페이스 생략 -->
        xmlns:local="clr-namespace:SetFocusInMvvm"
        Title="MainWindow" Height="450" Width="800">

	<Window.DataContext>
		<local:MainViewModel/>
	</Window.DataContext>
	
	<Grid>
		<StackPanel Width="500"
					Margin="10">

			<StackPanel Margin="5">
				<Label Content="User Name"/>
				<TextBox Text="{Binding UserName}"/>
			</StackPanel>

			<StackPanel Margin="5">
				<Label Content="Password"/>
				<TextBox Text="{Binding Password}"/>
			</StackPanel>

			<Button Content="Login"
					Width="100"
					Margin="5 10"
					Command="{Binding LoginCommand}"/>

			<TextBlock Text="{Binding Message}"
					   HorizontalAlignment="Center"
					   FontSize="12"
					   Foreground="Red"/>

			<TextBox x:Name="xFocusHandler"
					 Visibility="Hidden"
					 Text="{Binding FocusTargetPropertyName}"
					 TextChanged="FocusTargetPropertyChanged"/>
		</StackPanel>
	</Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();
	}

	private void FocusTargetPropertyChanged(object sender, TextChangedEventArgs e)
	{
		if (string.IsNullOrEmpty(xFocusHandler.Text))
		{
			e.Handled = true;
			return;
		}

		string propertyName = xFocusHandler.Text;
		// 대상 프로퍼티와 바인딩된 컨트롤을 논리적 트리에서 가져오는 부분
		// (DependencyObjectExtension.cs)
		var obj = this.FindBindingTarget(propertyName);
		if (obj is null)
		{
			return;
		}

		if (obj is FrameworkElement fe)
		{
			fe.Focus();
		}
		xFocusHandler.SetCurrentValue(TextBox.TextProperty, string.Empty);
	}
}

소스코드:

5개의 좋아요

비법 대 방출 이네요~~! :smile:

2개의 좋아요

@Nobody @루나시아 다양한 방법 감사합니다.

직접 사용해보며 좋은 코드를 볼 수 있는 좋은 기회였습니다!

4개의 좋아요