WPF ListView에서 항목을 효과적으로 제거하는 방법

현재 DevExpress MVVM 프레임워크를 이용해서 MVVM을 하고 있습니다.
Native ListView에 DataTemplate 형태로 button이 들어있는데, 이 버튼을 클릭했을 때 ListView에서 Item이 제거되게 하고 싶은데, 그러려면 조상 찾기로 DataContext를 참조해서 Command를 바인딩하거나, ListView에 DataContext를 따로 할당해서 Command를 바인딩 해야할 것 같습니다.

이 경우 어느 방법이 효율적일까요?

또한 제거를 한다고 했을 때 데이터를 어떤식으로 전달해서 제거할 수 있을까요?

5개의 좋아요

@Vincent 저도 개인적으로 ListBox나 DataGrid, TreeView 등에서 자주 사용하고, 또 고민하고 있어요. :smile:

저도 일반적으로 가장 가까운 조상(ViewModel) 찾아 처리하고 있습니다.
(되도록이면 Window, UserControl 수준의 큼직한 ViewModel에서 Command를 선언하고 있습니다.)

.xaml

<DataTemplate x:Key="ListViewItem1">
    <Button Command="{Binding 
        RelativeSource={RelativeSource AncestorType=ListView},
        Path=DataContext.ItemClickCommand}"
        CommandParameter="{Binding RelativeSource Self}"/>
</DataTemplate>

.cs

public class MyViewModel
{
    public ICommmand ItemClickCommand { get; set; }
    public ObservableCollection<UserModel> Users { get; set;}

    public MyViewModel()
    {
        Users = GetUsers();
        ItemClickCommand = new RelayCommand<Button>(ItemClick);   
    }

    private void ItemClick(Button item)
    {
        UserModel selected = (UserModel)item.DataContext;
        Users.Remove(selected);
    }
}

전 거의 이렇게 사용하고 있고요!

그리고 저는 무언가를 전달(CommandParameter)할때 UI객체를 보내면 포함된 DataContext까지 함께 활용할 수 있기 때문에 이 방법을 선호합니다.

그리고 컨트롤 Class를 상속받아 Command를 등록(DependencyProperty)하여 사용하기도 합니다.

<custom:JamesTreeView>
    <Setter Property="SelectedCommand" Value="{Binding UserSelectedCommand"/>
</custom:JamesTreeView>

이러면 컨트롤 내부에서 다양한 이벤트 동작을 섬세하게 판단하고, Command를 좀 더 자유롭게 동작(invoke) 시킬 수 있어서 좋은 것 같습니다.

5개의 좋아요

제 생각으로는 RelativeSource를 쉽게 사용하는게 별로 좋은 방법이 아닌거 같습니다.
RelativeSource를 쓰면 꽤(?) 복잡한 경로의 개체를 참조할 순 있지만 남용한다면 스파게티 참조가 이러납니다.

ViewModel에 직접적으로 ICommand 타입의 Remove용 커맨드를 속성으로 포함시키고 Binding 하는편은 어떠신가요.

@jamesnet214 님의 MyViewModelUserModel을 예로 든다면…

UserModelpublic ICommand RemoveCommand를 포함시키는 것입니다.
만약 UserModel이 도메인, 혹은 DTO 같은 다른 종류의 개체라 ICommand를 포함시킬 수 없다면
UserViewModel로 새로 만드는것을 추천드립니다.

오히려 MVVM의 ViewModel은 View의 추상화이기에 ViewModel을 만들어 사용가능한 명령(ICommand)까지 추상화하는게 좋다고 생각합니다.

추가적으로 전용 ViewModel을 만들긴 했지만, RemoveCommand를 위한 로직을 포함시키기 애매하다면 (종속따위와 같은 문제로) 외부에서 RemoveCommand에 대한 인스턴스를 생성자로 주입받을 수 있습니다.

예를 들어 아래 코드와 비슷합니다.

public class MyViewModel
{
    public ObservableCollection<UserViewModel> Users { get; set;}
    ....
}

public class UserViewModel
{
   Protected UserModel Source { get; }
   ....
   public ICommand RemoveCommand { get; }

   public UserViewModel(UserModel user, ICommand removeCommand)
   {
        this.Source = user;
        this.RemoveCommand = removeCommand;
   }
}
7개의 좋아요

저도 이 방법이 맞다고 생각합니다. ICommand 가 사실 그런 의도에 가깝게 만들어진 것 이라고 생각합니다.

4개의 좋아요

하지만 RelativeSource를 사용할 때 ViewModel이 없는 하위에서 상위 ViewModel을 찾는 것은 괜찮은 방법이지 않을까요? 이 방식을 지지하시는 분도 나오셨으면 좋겠습니다… :smile:

@SangHyeon.Kim
그리고 아무래도 계속 해왔던 방식이 익숙하다 보니 ListViewItem과 같은 부분에 Model이 아닌 생성자가 포함된 ViewModel을 사용하는 것이 아직도 약간 어색하다고 느껴지는데요. 저도 많이 생각해보고 배워야 할 것 같습니다.

저도 큰 도움이 되는 것 같습니다. :smile:

적극 사용해보겠습니다. 좋은 설명 감사합니다!!

3개의 좋아요

저는 UI와 직결되는 기능은 VM이 아닌 비하인드 코드쪽에서 처리하는게 맞지 않나… 생각합니다.

제가 생각한 방식은 ListView의 새 아이템이 생성될 이벤트에서 새롭게 생성하는 버튼 객체에 버튼클릭 이벤트를 추가하는 것 입니다.

WPF에서 UI 작업은 메인 스레드에서 이뤄져야 하고 이를 VM에서 처리하는 것은 비동기쪽 다룰 때 문제가 발생할 확률이 높다고 생각하기 때문입니다.

지금은 컴퓨터를 못 써서 확인을 못 해봤는데 내일 이 방법으로 해보고 이 내용에 업데이트 하겠습니다.

3개의 좋아요

아 근데 말씀해주신것도 좋은 방법이라 생각합니다! ㅎㅎㅎ
제가 요즘 좀 WPF좀 해보니…
MVVM의 ViewModel은 View의 추상화인데 어느 정도 수준까지 추상화하는지는 개인마다 틀린거 같습니다.

저 같은 경우에는 View의 모든 데이터(Public Property)와 동작(ICommand)을 ViewModel로 추상화합니다.

이 경우 해당 View가 갖추어야하는 모든 Logic 코드가 ViewModel에 존재하게 되는데,
이럴 경우 온전한 단위테스트가 가능하며, 해당 ViewModel에 어떠한 껍데기 (Xaml로 만든 View)를 만들어다 붙여도 동작에 변함과 이상이 없습니다.

특정 ViewModel에 어떠한 View(Xaml)로 UI를 표현해 갖다 붙여도 동작에 변함과 이상이 없다는건
MVVM이 추구하는 Presentation과 Presentation Logic이 완벽히 분리되었음을 의미하기도 합니다.

근데 뭐… MVVM은 참 개인의 차이가 큰거 같습니다.

4개의 좋아요

저도 level120님 말씀처럼 뷰에 관한 로직은 이벤트로 처리하는게 맞다고 생각하긴 하는데…커맨드를 이벤트로 바인딩하는걸 프레임워크를 통해 사용할 수 있다보니 제로 코드비하인드를 좀 추구하는 편이기도 합니다. 말씀하신 단위테스트 이유에서도 mvvm은 꼭 필요하다고 생각하고 그것은 곧 생산성(유지보수)에도 포함된다고 생각합니다.

1개의 좋아요

저도 뷰에 관한 로직은 이벤트로 해도 무방하다고 생각하는 편입니다. 근데 뭔가 로직의 관리포인트가 뷰모델에도 있고 코드비하인드에도 있다고하면 유지보수가 힘들거같다고 동시에 생각도 합니다… 혹시 이 부분은 어떻게 생각하시는지요…?

1개의 좋아요

혹시 조상찾기로 커맨드를 바인딩했을때 성능이슈가 있다고 아티클에서 보긴했는데 조상 찾기 방식을 자주 사용하신다면 그런 부분이 체감되는 수준인가요? 그게 실재하는 것인지 궁금하네요…ㅎㅎ

1개의 좋아요

@Vincent 제가 체감하기에는 크게 문제는 없어보입니다! :smile:
근데 저는 실제로 RelativeSource를 ControlTemplate 단위에서 간단간단하게 사용하고 있기 때문에 체감될만한 케이스가 없었을 수도 있을 것 같아요.

그리고 ListViewitem과 같은 반복 부분 템플릿에서는 컨트롤에서 ICommand를 DependencyProperty로 등록해서 사용하고 있습니다.

1개의 좋아요

답변 감사드립니다. 내용은 20자 이상이어야 합니다.

1개의 좋아요

@Vincent 맞습니다. 근데 정말 정답이 없는 것 같아요.

저는 WPF가 나온 해 부터 오로지 이것만 했는데도 결국 매 해, 매 순간 정답이 바뀌는 것만 같습니다. :joy:

그럼에도 제 개인적으로는 기본적으로 MVVM을 고수해야 한다고 생각합니다.
ViewModel에서 부족한 부분(이벤트를 포함한)은 각각의 핵심 컨트롤들이 DependencyPropery 추가를 통해 ViewModel 규칙을 깨지 않도록 단단하게 보완하고, ContentControl 기반의 Template이 중심이 되는 구조를 이상적으로 생각하고 있습니다.

그리고 계속 거듭하며 보완하고 토론하고 고쳐나갈 수 있는 것에 큰 기대가 들어요!!

3개의 좋아요

winform 위주로 작업하다 MVVM으로 WPF작업중입니다.
지금 제가 맞닥드린 문제인데요.
결론적으론 RelativeSource로 처리했습니다.
제 경우엔 단위테스트도 크게 필요없는지라 ViewModel을 추가해 만들지 않고 처리했습니다.
ViewModel을 새로 만드는게 맞는것 같은데 제 경우엔 케바케인듯 합니다.
:slight_smile:

3개의 좋아요

오래된 게시물에 답글 달아서 끌올! ㅋㅅㅋ~

@jamesnet214 근데

요기서 CommandParameter="{Binding RelativeSource Self}" 하시면 안 됩니닷
이렇게 하면 작성하신 MyViewModel 처럼

ViewModel 안에서 View 객체를 획득하게 되지욤. 이러면 패턴을 깨는 행위가 되어욜.+ㅁ+ ;;

만약 ListViewItem 의 DataContext 를 넘기고 싶다면

CommandParameter="{Binding}"

요러케만 하셔도 되겠죵?

그리고 @SangHyeon.Kim 하신 방법이 사실 가장 정석적인 방법인데
뎁스가 깊어지다보면 Model 을 직접 ViewModel 로 사용하는 ItemTemplate 들이 생겨나게 되지요.

예를 들면

public class UserGroup
{
	public ObservableCollection<User> Users { get; }	 // ListView 의 ItemsSource 로 binding
}

public class User // <-- 결국 이 타입이 ListViewItem 의 ViewModel 이 됨.
{
	public string Id { get; set; }
	public string Name { get; set; }
}

요런식으로 간단하게 예를 들자면
RelativeSource 없이 Remove command 을 정의한다고 하면
ICommand 와 그 멤버 선언을 User 타입에 해줘야 하는 상황이 발생해요.

public class User // 요따우 로?
{
   ...
	private RelayCommand _removeCommand;
	public ICommand RemoveCommand => _removeCommand ?? (_removeCommand = new RelayCommand(Remove, CanRemove));

	private void Remove(object user)
	{
		// 부모 UserGroup 에서 자기 자신 삭제?
	}
}

이건 논란의 여지가 있을 수 있는데
User 타입을 단순히 Model 의 컨텍스트 정도로 바라본다면
User 타입 내부에 UserGroup 의 View 를 핸들링하는 Command 를 작성하는 건 적절하지 않을 수 있어요.

그래서 개인적으로는 RelativeSource 를 이용하는 게 일반적으로는 맞다고 보는데

이게 복잡한 템플릿 속에서 막 이것저것 연결되다보면
그 RelativeSource 로 부모와 조상을 찾는 것에서 버그가 발생할 가능성이 높기도 하거등요.

그래서 저는 간단한 구조일 경우에는 RelativeSource 를 이용해 Command binding 을 하고
복잡하거나 정리가 필요한 상황에서는 별도의 Command Context 를 static 으로 정의해 사용하는 편이에욤

public static class UserGroupCommandBag
{
	private static RelayCommand _removeUserCommand;
	public static ICommand RemoveUserCommand => _removeCommand ?? (_removeCommand = new RelayCommand(Remove, CanRemove));
	
	private static void RemoveUser(object user)
	{
		// 요기서 UserGroup 객체를 찾아서 직접 처리하거나 
		// ObservableRecepient or pubsub같은 걸루 메시지를 쏘고 그걸 UserGroup 에서 받아서 처리합니닷
	}
}

요런거 하나 정의해두고 view 에서 사용할 때에는

<DataTemplate x:Key="ListViewItem1">
    <Button Command="{x:Static UserGroupCommandBag.RemoveUserCommand}" 
CommandParameter="{Binding}"/>
</DataTemplate>

요런 식으루 사용해열. ㅇㅅㅇ/
(예시로 든건 의사코드입니닷. 아이디어 차원에서 봐주세욤 ㅋㅅㅋ)






와…

써놓고 보니 엄청 길군요… -ㅂ-;;;

7개의 좋아요

만약 Model을 컬렉션으로 사용하는 ViewModel이 하나뿐이거나, 삭제 기능을 이용하는 ViewModel이 하나뿐이라면 Messenger를 이용하는 것도 방법 중 하나일 것 같네요. @Greg.Lee 님의 코드와 같이 Model에 직접 Command를 정의하는 경우라면 커맨드파라미터 대신 본인을 message의 파라미터로 해서 메시지를 전달하는 방식인 거죠.

Community.Toolkit.Mvvm에서 제공하는 Message 형식을 이용해 보면

public class UserModel : ObservableRecipient
{
	//...
	private void Remove()
	{
		Messenger.Send(new UserModelDeleteMessage(this));
	}
}

public class UserModelDeleteMessage : ValueChangedMessage<UserModel>
{
	public UserModelDeleteMessage(UserModel target) : base(target)
	{
	}
}

이런 식으로 메시지를 전송하고, 해당 Message를 Register한 ViewModel에서 메시지를 수신할 때 UserModel을 받아 컬렉션에 포함되어 있으면 제거하는 방식으로 처리할 수 있을 것 같네요.

물론 UserModel을 사용하는 ViewModel이 다수거나 삭제 기능을 이용하는 ViewModel이 다수고, 서로 영향을 주지 않아야 한다면 써먹기 힘든 방법이지만요.

5개의 좋아요

@루나시아 아, 오해하실까봐 첨언드리자묜

저는 개인적으로 Model 은 그 자체로 Context 로만 사용해야하자는 주의랍니다. ㅇㅅㅇ*
그래서 위에서 제가 제시했던 Model 에 command 정의하는 상황은

avoid 해야하는 상황인 거죠.

비록 Model 이 ItemsTemplate 에서 ViewModel 로 사용된다 할지라도
직접 Command 나 기타 다른 로직을 수행하는 기능을 넣으면 안 된다는 얘기이옵니닷 =ㅂ=~

그래서 별도의 CommandBag 을 만든거구욤.

5개의 좋아요

그렇군요.

써놓고 보니 MVVM 아키텍처에서, 데이터를 담는 객체인 Model에서 본인을 핸들링하는 객체에 자신을 삭제해달라는 Message를 쏘는 건 뭔가 모순 같기도 하네요. 처음부터 ViewModel로 정의된 객체가 아니라 ViewModel 안에서 Model을 직접 ViewModel로 쓰게 되는 상황에서는 말씀하신 것처럼 별도의 command 집합을 정의해놓고 바인딩하는 게 좀 더 깔끔한 방법인 듯합니다.

5개의 좋아요

제가 썼던 글도 어색하게 느껴지네요 ㅎㅎ
모두 좋은 방법인 것 같습니다.

MVVM은 개발 방법이기 때문에
바인딩이나 커맨드를 어떻게 사용하든 약간의 차이는 문제 없다고 생각해요~ 그리고 단순한 처리부터 복잡한 처리까지 업무나 목적도 정말 다양하고요.

다양한 프로젝트를 하다보면 분명 한가지 방법으로만 할 수는 없더라고요. 상황에 따라 유연하게 잘 해결해 나가면 되지 않을까요. :smile:

하지만 더 좋고 재밌는 방법은 늘 있는 것 같습니다. 특히나 WPF는 매번 새로운 것 같아요. 닷넷데브에 계신 많은 분들의 지식이 지금처럼 계속 쌓여가다 보면 얼마나 멋질지 잠깐 상상해봤습니다. :slight_smile:

4개의 좋아요