티스토리 뷰

반응형

ComboBox와 ListBox 컨트롤을 처음 접하거나, MVVM 패턴으로 사용하는 것이 익숙하지 않은 개발자를 위해 컨트롤 사용 방법을 자세하게 포스팅 하도록 하겠습니다.

 

1. Overview

ComboBox

: 오른쪽에 아래 화살표(?)를 클릭하여 드롭다운 목록을 보이도록 한 후 원하는 아이템을 선택할 수 있는 컨트롤로 버튼을 눌러야 데이터의 목록이 출력되며, 무조건 1개만 선택 할 수 있습니다.

더 자세한 사항은 여기를 참고하시기 바랍니다.

ListBox

: 데이터의 목록을 출력한 후 아이템을 선택할 수 있는 컨트롤로 처음부터 데이터 목록 보이며, 1개 혹은 여러개의 아이템을 선택할 수 있습니다.

더 자세한 사항은 여기를 참고하시기 바랍니다.

2. 필수 프로퍼티

ComboBox, ListBox를 사용하려면 반드시 아래 프로퍼티에 대한 내용은 알아야 합니다.

 

ItemsSource : 목록을 생성하는데 필요한 컬렉션을 가져오거나 설정할 수 있습니다.

SelectedItem : 선택된 아이템을 가져오거나 설정할 수 있습니다.

SelectedValue : SelectedItem에 설정된 데이터에서, SelectedValuePath에 입력된 프로퍼티의 값을 가져오거나 설정할 수 있습니다.

SelectedIndex : 선택된 아이템 혹은 아이템들 중 첫번째 항목의 인덱스를 가져오거나 설정할 수 있습니다. 선택된 아이템이 존재 하지 않는 경우 -1을 반환합니다.

DisplayMemberPath : 선택된 아이템에서 출력할 프로퍼티의 이름을 가져오거나 설정할 수 있습니다.

SelectedValuePath : 선택된 아이템에서 값을 가져올 프로퍼티의 이름을 가져오거나 설정할 수 있습니다.

3. 기본 설정

Wpf 프로젝트를 추가하고, Microsoft.Toolkit.Mvvm 누겟 패키지도 추가했습니다.

 

4. ItemsSource 사용

MainWindow.xaml

기본적인 내용 추가하면서 ComboBox와 ListBox에 ItemsSource에 Persons를 바인딩했습니다.

<Window
    x:Class="BasicControlSample.MainWindow"
    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:local="clr-namespace:BasicControlSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Text="ListBox1" />
        <ListBox Grid.Row="1" ItemsSource="{Binding Persons}" />
        <TextBlock Grid.Row="2" Text="ComboBox1" />
        <ComboBox
            Grid.Row="3"
            VerticalAlignment="Top"
            ItemsSource="{Binding Persons}" />
    </Grid>
</Window>

MainWindow.xaml.cs

DataContext에 MainViewModel을 생성해서 넣어 주었습니다. 이 방법은 셈플 프로젝트에서 간단하게 뷰모델을 생성해서 넣기 위해사 사용하는 방법으로 실제 사용법은 아래 링크를 참고하시기 바랍니다.

2022.02.21 - [WPF] - Microsoft.Toolkit.Mvvm을 이용한 간단한 프레임워크 part1

using System.Windows;

namespace BasicControlSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainViewModel();
        }
    }
}

MainViewModel.cs

뷰모델을 간단하게 만들었습니다.

using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BasicControlSample
{
    public class MainViewModel : ObservableObject
    {
        private IList<Person> _persons = new List<Person>
            {
                new Person{ Name = "kaki0104", Sex = true, Age = 11, Address = "Seoul1" },

                ...

                new Person{ Name = "kaki0143", Sex = false, Age = 150, Address = "Seoul140" },
            };
        public IList<Person> Persons { get { return _persons; } }

        public MainViewModel()
        {
            Init();
        }

        private void Init()
        {
            
        }
    }
}

실행 화면

콤보박스가 비어있는 것은 SelectedIndex가 -1로 현재 선택된 아이템이 존재하지 않기 때문입니다. 그래서, 역삼각형을 눌르서 DropDown 리스트를 보이도록 했습니다.

리스트박스는 뷰모델의 Persons의 데이터 목록이 출력되는 것을 확인할 수 있습니다.

그런데 이렇게 출력되는 것을 원하는 것은 아니겠죠?

5. DisplayMemberPath 사용하기

콤보박스와 리스트박스에 DisplayMemberPath를 추가했습니다. 화면에 보여줄 프로퍼티는 Name입니다.

        <!--  DisplayMemberPath 추가  -->
        <TextBlock
            Grid.Row="0"
            Grid.Column="1"
            Text="ListBox1" />
        <ListBox
            Grid.Row="1"
            Grid.Column="1"
            DisplayMemberPath="Name"
            ItemsSource="{Binding Persons}" />
        <TextBlock
            Grid.Row="2"
            Grid.Column="1"
            Text="ComboBox1" />
        <ComboBox
            Grid.Row="3"
            Grid.Column="1"
            VerticalAlignment="Top"
            DisplayMemberPath="Name"
            ItemsSource="{Binding Persons}" />

실행 화면

콤보박스와 리스트박스에 Person 모델의 Name 프로퍼티 값이 출력됩니다.

이제 DisplayMemberPath가 어떤 것인지 이해가 되시죠?

6. SelectedItem 사용하기1

콤보박스와 리스트박스에서 선택된 아이템을 가져오거나 설정할 수 있다고 했는데 아래 코드를 확인해 보죠

MainWindow.xaml

SelectedItem 프로퍼티를 SelectedListitem과 SelectedComboItem으로 바인딩을 하도록 되어있습니다.

상단 우측에는 SelectedListItem.Name과 SelectedComboItem.Name이 출력되도록 TextBlock를 추가했습니다.

그렇다면, 뷰모델에 해당 프로퍼티를 추가해 주어야 겠죠?

        <TextBlock
            Grid.Row="0"
            Grid.Column="2"
            Text="ListBox3" />
        <TextBlock
            Grid.Row="0"
            Grid.Column="2"
            HorizontalAlignment="Right"
            Text="{Binding SelectedListItem.Name}" />
        <ListBox
            Grid.Row="1"
            Grid.Column="2"
            DisplayMemberPath="Name"
            ItemsSource="{Binding Persons}"
            SelectedItem="{Binding SelectedListItem}" />
        <TextBlock
            Grid.Row="2"
            Grid.Column="2"
            Text="ComboBox3" />
        <TextBlock
            Grid.Row="2"
            Grid.Column="2"
            HorizontalAlignment="Right"
            Text="{Binding SelectedComboItem.Name}" />
        <ComboBox
            Grid.Row="3"
            Grid.Column="2"
            VerticalAlignment="Top"
            DisplayMemberPath="Name"
            ItemsSource="{Binding Persons}"
            SelectedItem="{Binding SelectedComboItem}" />

MainViewModel.cs

SelectedListitem와 SelectedComboItem 프로퍼티를 추가하고, 초기값을 Persons의 첫번째 아이템으로 입력했습니다.

        public Person SelectedListItem { get; set; }

        public Person SelectedComboItem { get; set; }

        public MainViewModel()
        {
            Init();
        }

        private void Init()
        {
            SelectedListItem = Persons.FirstOrDefault();
            SelectedComboItem = Persons.FirstOrDefault();
        }

실행화면

리스트박스에 맨 처음 아이템이 선택된 것으로 표시되며, 콤보박스에도 첫번째 아이템이 선택된 것으로 표시 됩니다.

뷰모델의 프로퍼티가 뷰에 영향을 준것을 확인할 수 있습니다.

7. SelectedItem 사용하기2

좀전의 콤보박스와 리스트박스에서 선택된 아이템을 변경하면 어떻게 될까요?

화면에서는 선택된 아이템이 변경되지만, SelectedListItem과 SelectedComboItem은 변경되지 않습니다.

화면과 뷰모델의 데이터가 서로 달라진 것을 확인할 수 있습니다. 왜 이러는 걸까요?

문제는 우리가 바인딩을 TwoWay로 하지 않았기 때문입니다. TextBox를 제외한 대부분의 컨트롤은 바인딩 기본값이 OneWay이기 때문입니다.

 

MainWindow.xaml

아래와 같이 TwoWay로 변경 후 다시 실행합니다.

SelectedItem="{Binding SelectedListItem, Mode=TwoWay}"

SelectedItem="{Binding SelectedComboItem, Mode=TwoWay}"

이렇게 수정하고, 실행 후 다른 아이템을 선택해도, 화면은 그대로 입니다. 

이유는?

SelectedListItem과 SelectedComboItem의 값은 변경되었지만, 뷰에서 그 프로퍼티들이 변경되었는지 여부를 판단할 수 없어서, 화면을 갱신시키지 않았기 때문입니다.

뷰가 화면을 갱신 시키기 위해서는 프로퍼티 체인지 이벤트가 필요합니다. 이제 각 프로퍼티들이 프로퍼티 체인지 이벤트를 발생시킬 수 있도록 수정해 줍니다.

 

MainViewModel.cs

        private Person _selectedListItem;
        public Person SelectedListItem
        {
            get { return _selectedListItem; }
            set { SetProperty(ref _selectedListItem, value); }
        }

        private Person _selectedComboItem;
        public Person SelectedComboItem
        {
            get { return _selectedComboItem; }
            set { SetProperty(ref _selectedComboItem, value); }
        }

리스트박스에 SelectedItem 프로퍼티가 변경되면, 뷰모델의 SelectedListItem 프로퍼티를 변경시키게되고, 그러면 SelectedListitem이 변경되었다는 프로퍼티 체인지 이벤트가 발생하고, 그 이벤트를 다시 뷰에 TextBlock에 바인딩되어있는 곳에서 확인하고 화면을 갱신 시켜줍니다.

8. 뷰모델에서 SelectedItem 변경여부 확인하기

리스트박스의 SelectedItem이 변경되면 SelectedListItem의 값을 변경할수 있다는 것까지는 알겠는데, 변경된 사실을 뷰모델 내부에서는 어떻게 알 수 있죠?

여기에는 2가지 방법이 존재합니다.

  • 1) SelectionChanged 이벤트를 뷰모델에 전달해서 아는 방법
  • 2) 뷰모델 내부에 PropertyChanged 이벤트 핸들러를 추가해서 아는 방법

일반적으로는 2)번 방법을 대부분 사용하고, 1)번은 꼭 사용해야 하는 한정적인 상황에서만 사용합니다. 예를들어 SelectionChanged 이벤트 Arguments를 뷰모델에서 꼭 알아내서 사용해야 하는 경우입니다. 

그동안 위의 두가지 방법에 대해서 정확한 개념을 못잡았던 분들은 참고하시면 좋을 것 같습니다.

여기서는 2) 방법을 이용해서 SelectedItem이 변경되었는지 여부를 확인하고, SelectedItem이 변경되었을 때 Command의 사용가능 여부를 확인하는 코드를 작성하도록 하겠습니다.

 

MainWindow.xaml

  • 컨트롤 오른쪽 상단에 삭제 Button을 하나씩 추가하고, Command는 뷰모델에 있는 커맨트를 바인딩하도록 했습니다.
  • ListBox는 SelectedListItem2라는 프로퍼티와 TwoWay 바인딩을 했습니다.
  • ComboBox는 SelectedComboItem2라는 프로퍼티와 TwoWay 바인딩을 했습니다.
        <!--  SelectedItem과 뷰모델 연동  -->
        <TextBlock
            Grid.Row="0"
            Grid.Column="3"
            Text="ListBox4" />
        <Button
            Grid.Row="0"
            Grid.Column="3"
            HorizontalAlignment="Right"
            Command="{Binding DeleteListItemCommand}"
            Content="Delete ListItem" />
        <ListBox
            Grid.Row="1"
            Grid.Column="3"
            DisplayMemberPath="Name"
            ItemsSource="{Binding Persons}"
            SelectedItem="{Binding SelectedListItem2, Mode=TwoWay}" />
        <TextBlock
            Grid.Row="2"
            Grid.Column="3"
            Text="ComboBox4" />
        <Button
            Grid.Row="2"
            Grid.Column="3"
            HorizontalAlignment="Right"
            Command="{Binding DeleteComboItemCommand}"
            Content="Delete ComboItem" />
        <ComboBox
            Grid.Row="3"
            Grid.Column="3"
            VerticalAlignment="Top"
            DisplayMemberPath="Name"
            ItemsSource="{Binding Persons}"
            SelectedItem="{Binding SelectedComboItem2, Mode=TwoWay}" />

MainViewModel.cs

  • SelectedListItem2, SelectedComboItem2 프로퍼티와 DeleteListItemCommand, DeleteComboItemCommand를 각각 추가합니다.
  • DeleteListItemCommand와 DeleteComboItemCommand는 Init메소드에서 생성하면서, 사용가능 조건을 추가했습니다.
  • 커맨드를 생성할 때 사용가능 조건을 입력해서, 실행 중에 바인딩되어있는 버튼의 Enabled 속성을 변경시킬 수 있습니다.
  • PropertyChanged += MainViewModel_PropertyChanged; 를 이용해서, 뷰모델 내부에서 발생되는 프로퍼티 체인지 이벤트를 핸들링 합니다.
  • case nameof(SelectedListItem2), case nameof(SelectedComboItem2)를 통해서 해당 프로퍼티가 변경되면, 실행할 코드를 미리 입력해 놓습니다.
  • NotifyCanExecuteChanged() 메소드는 커맨드의 사용가능 여부를 재확인하는 메소드 입니다.
        private Person _selectedListItem2;
        public Person SelectedListItem2
        {
            get { return _selectedListItem2; }
            set { SetProperty(ref _selectedListItem2, value); }
        }

        public IRelayCommand DeleteListItemCommand { get; set; }

        private Person _selectedComboItem2;
        public Person SelectedComboItem2
        {
            get { return _selectedComboItem2; }
            set { SetProperty(ref _selectedComboItem2, value); }
        }

        public IRelayCommand DeleteComboItemCommand { get; set; }

        private void Init()
        {
            ...
            //커맨드 생성
            DeleteListItemCommand = new RelayCommand(OnDeleteListItem, 
                () => SelectedListItem2 != null && SelectedListItem2.Age % 2 == 0);
            DeleteComboItemCommand = new RelayCommand(OnDeleteComboItem, 
                () => SelectedComboItem2 != null && SelectedComboItem2.Age % 2 == 1);

            //뷰모델 내부에서 프로퍼티 체인지 이벤트 핸들러 추가
            PropertyChanged += MainViewModel_PropertyChanged;
            
            ...
        }
            
        private void MainViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            //nameof를 이용하면, 프로퍼티의 이름이 변경되었을 때 에러가
            //발생하기 때문에, 에러가 발생하는 것을 방지 할 수 있음
            switch (e.PropertyName)
            {
                case nameof(SelectedListItem2):
                    //커맨드의 사용가능 여부 확인
                    DeleteListItemCommand.NotifyCanExecuteChanged();
                    break;
                case nameof(SelectedComboItem2):
                    DeleteComboItemCommand.NotifyCanExecuteChanged();
                    break;
            }
        }

        private void OnDeleteComboItem()
        {
            Persons.Remove(SelectedComboItem2);
        }

        private void OnDeleteListItem()
        {
            Persons.Remove(SelectedListItem2);
        }

실행화면

처음 실행시에는 2개의 버튼 모두 사용할 수 없는 상태로 출력됩니다. 이유는 버튼에 바인딩된 커맨드의 사용가능 여부가 false이기 때문입니다.

리스트 박스의 첫번째 아이템을 선택하고, 콤보박스에서도 첫번째 아이템을 선택했습니다. 콤보박스의 첫번째 아이템을 선택하니, Delete ComboItem 버튼이 사용가능 상태로 변경된 것이 확인 됩니다.

버튼을 클릭해서 선택된 아이템을 삭제합니다.

선택되었던 아이템이 삭제되면서, ComboBox에 선택된 아이템은 null로 변경되고, 버튼은 다시 사용하지 못하는 상태로 변경됩니다.

MainViewModel.cs 전체 소스

_persons의 목록은 2개만 추가했습니다.

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace BasicControlSample
{
    public class MainViewModel : ObservableObject
    {
        private IList<Person> _persons = new ObservableCollection<Person>
            {
                new Person{ Name = "kaki0104", Sex = true, Age = 11, Address = "Seoul1" },
                new Person{ Name = "kaki0143", Sex = false, Age = 150, Address = "Seoul140" },
            };
        public IList<Person> Persons { get { return _persons; } }

        private Person _selectedListItem;
        public Person SelectedListItem
        {
            get { return _selectedListItem; }
            set { SetProperty(ref _selectedListItem, value); }
        }

        private Person _selectedComboItem;
        public Person SelectedComboItem
        {
            get { return _selectedComboItem; }
            set { SetProperty(ref _selectedComboItem, value); }
        }

        private Person _selectedListItem2;
        public Person SelectedListItem2
        {
            get { return _selectedListItem2; }
            set { SetProperty(ref _selectedListItem2, value); }
        }

        public IRelayCommand DeleteListItemCommand { get; set; }

        private Person _selectedComboItem2;
        public Person SelectedComboItem2
        {
            get { return _selectedComboItem2; }
            set { SetProperty(ref _selectedComboItem2, value); }
        }

        public IRelayCommand DeleteComboItemCommand { get; set; }

        public MainViewModel()
        {
            Init();
        }

        private void Init()
        {
            SelectedListItem = Persons.FirstOrDefault();
            SelectedComboItem = Persons.FirstOrDefault();

            //커맨드 생성
            DeleteListItemCommand = new RelayCommand(OnDeleteListItem,
                () => SelectedListItem2 != null && SelectedListItem2.Age % 2 == 0);
            DeleteComboItemCommand = new RelayCommand(OnDeleteComboItem,
                () => SelectedComboItem2 != null && SelectedComboItem2.Age % 2 == 1);

            //뷰모델 내부에서 프로퍼티 체인지 이벤트 핸들러 추가
            PropertyChanged += MainViewModel_PropertyChanged;
        }

        private void MainViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            //nameof를 이용하면, 프로퍼티의 이름이 변경되었을 때 에러가
            //발생하기 때문에, 에러가 발생하는 것을 방지 할 수 있음
            switch (e.PropertyName)
            {
                case nameof(SelectedListItem2):
                    //커맨드의 사용가능 여부 확인
                    DeleteListItemCommand.NotifyCanExecuteChanged();
                    break;
                case nameof(SelectedComboItem2):
                    DeleteComboItemCommand.NotifyCanExecuteChanged();
                    break;
            }
        }

        private void OnDeleteComboItem()
        {
            Persons.Remove(SelectedComboItem2);
        }

        private void OnDeleteListItem()
        {
            Persons.Remove(SelectedListItem2);
        }
    }
}

9. 소스

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

 

GitHub - kaki104/WpfTest

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

github.com

part2에서는 SelectionChanged 이벤트를 이용하는 방법, SelectedValue, SelectedValuePath, SelectedIndex 사용법에 대해서 설명하도록 하겠습니다.

 

반응형
댓글