MVVM을 사용하여 WPF ListView 항목에서 두 번 클릭 이벤트 발생


102

MVVM을 사용하는 WPF 응용 프로그램에서 목록보기 항목이있는 사용자 컨트롤이 있습니다. 런타임에는 데이터 바인딩을 사용하여 목록보기를 개체 컬렉션으로 채 웁니다.

목록보기의 항목에 더블 클릭 이벤트를 첨부하여 목록보기의 항목을 두 번 클릭하면보기 모델의 해당 이벤트가 발생하고 클릭 된 항목에 대한 참조를 갖도록하는 올바른 방법은 무엇입니까?

어떻게 깨끗한 MVVM 방식으로 할 수 있습니까?

답변:


76

코드 숨김은 전혀 나쁜 것이 아닙니다. 불행히도 WPF 커뮤니티의 많은 사람들이이 문제를 잘못 알고 있습니다.

MVVM은 뒤에있는 코드를 제거하는 패턴이 아닙니다. 뷰 파트 (모양, 애니메이션 등)와 로직 파트 (워크 플로)를 분리하는 것입니다. 또한 논리 부분을 단위 테스트 할 수 있습니다.

데이터 바인딩이 모든 것에 대한 해결책이 아니기 때문에 코드를 작성해야하는 시나리오를 충분히 알고 있습니다. 귀하의 시나리오에서는 파일 뒤에있는 코드에서 DoubleClick 이벤트를 처리하고이 호출을 ViewModel에 위임합니다.

코드 숨김을 사용하고 MVVM 분리를 수행하는 샘플 응용 프로그램은 여기에서 찾을 수 있습니다.

WPF 응용 프로그램 프레임 워크 (WAF) - http://waf.codeplex.com


5
글쎄요, 두 번 클릭하기 위해 모든 코드와 추가 DLL을 사용하지 않습니다!
Eduardo Molteni

4
이것은 바인딩 만 사용하는 것이 나에게 진정한 두통을 안겨줍니다. 팔 1 개, 눈 패치 1 개, 다리 1 개로 코드를 작성하라는 요청을받는 것과 같습니다. 더블 클릭은 간단해야하며이 모든 추가 코드가 그만한 가치가 있는지 모르겠습니다.
Echiban 2010

1
나는 당신에게 전적으로 동의하지 않는 것이 두렵습니다. '코드 비하인드가 나쁘지 않다'고 말하면 그에 대한 질문이 있습니다. 버튼에 대한 클릭 이벤트를 위임하지 않고 대신 종종 바인딩 (Command 속성 사용)을 사용하는 이유는 무엇입니까?
Nam G VU

21
@Nam Gi VU : WPF Control에서 지원하는 경우 항상 Command Binding을 선호합니다. 명령 바인딩은 'Click'이벤트를 ViewModel (예 : CanExecute)에 전달하는 것 이상을 수행합니다. 그러나 명령은 가장 일반적인 시나리오에서만 사용할 수 있습니다. 다른 시나리오의 경우 코드 숨김 파일을 사용할 수 있으며 여기에서 UI와 관련이없는 문제를 ViewModel 또는 Model에 위임합니다.
jbe 2010-07-18

2
이제 더 많이 이해합니다! 당신과 좋은 토론!
Nam G VU

73

.NET 4.5에서 작동하도록 할 수 있습니다. 똑바로 보이며 타사 또는 코드가 필요하지 않습니다.

<ListView ItemsSource="{Binding Data}">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid Margin="2">
                    <Grid.InputBindings>
                        <MouseBinding Gesture="LeftDoubleClick" Command="{Binding ShowDetailCommand}"/>
                    </Grid.InputBindings>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Image Source="..\images\48.png" Width="48" Height="48"/>
                    <TextBlock Grid.Row="1" Text="{Binding Name}" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

2
전체 영역에서 작동하지 않는 것 같습니다. 예를 들어 도크 패널에서이 작업을 수행하고 도크 패널 내에 무언가 (예 : 텍스트 블록, 이미지)가 있고 빈 공간이 아닌 경우에만 작동합니다.
Stephen Drew

3
OK -이 오래된 밤나무 다시 ... 필요에 따라, 마우스 이벤트를 수신하기 위해 투명 배경을 설정 stackoverflow.com/questions/7991314/...
스티븐 드류에게

6
나는 그것이 왜 내가 아닌 여러분 모두에게 효과가 있는지 알아 내려고 머리를 긁적 거리고있었습니다. 갑자기 항목 템플릿의 컨텍스트 내에서 데이터 컨텍스트가 기본 창의 뷰 모델이 아니라 itemssource의 현재 항목이라는 것을 깨달았습니다. 그래서 다음을 사용하여 작동하도록했습니다. <MouseBinding MouseAction = "LeftDoubleClick"Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x : Type Window}}}"/> 제 경우에는 EditBandCommand가 바인딩 된 엔터티가 아닌 페이지의 뷰 모델에있는 명령입니다.
naskew

naskew는 MVVM Light에 필요한 비밀 소스를 가지고 있었고, 두 번 클릭 한 목록 상자 항목에서 모델 개체가되는 명령 매개 변수를 가져오고, 창의 데이터 컨텍스트는 명령을 노출하는보기 모델로 설정됩니다. <MouseBinding Gesture = "LeftDoubleClick "Command ="{Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x : Type Window}}} "CommandParameter ="{Binding} "/>
MC5

InputBindings.NET 3.0 에서 사용할 수 있고 Silverlight 에서는 사용할 수 없는 것을 추가하고 싶습니다 .
Martin

44

첨부 된 명령 동작 및 명령 을 사용하고 싶습니다 . Marlon Grech 는 Attached Command Behaviors를 아주 잘 구현했습니다. 이를 사용하여 각 ListViewItem에 대한 명령을 설정하는 ListView의 ItemContainerStyle 속성에 스타일을 할당 할 수 있습니다.

여기서는 MouseDoubleClick 이벤트에서 실행될 명령을 설정하고 CommandParameter는 클릭하는 데이터 개체가됩니다. 여기에서는 사용중인 명령을 얻기 위해 시각적 트리를 살펴 보지만 응용 프로그램 전체의 명령을 쉽게 만들 수 있습니다.

<Style x:Key="Local_OpenEntityStyle"
       TargetType="{x:Type ListViewItem}">
    <Setter Property="acb:CommandBehavior.Event"
            Value="MouseDoubleClick" />
    <Setter Property="acb:CommandBehavior.Command"
            Value="{Binding ElementName=uiEntityListDisplay, Path=DataContext.OpenEntityCommand}" />
    <Setter Property="acb:CommandBehavior.CommandParameter"
            Value="{Binding}" />
</Style>

명령의 경우 ICommand를 직접 구현 하거나 MVVM Toolkit 에있는 것과 같은 일부 도우미를 사용할 수 있습니다 .


1
+1 WPF (Prism) 용 Composite Application Guidance로 작업 할 때 이것이 제가 선호하는 솔루션이라는 것을 알게되었습니다.
Travis Heseman 2010

1
위의 코드 샘플에서 네임 스페이스 'acb :'는 무엇을 의미합니까?
Nam G VU

@NamGiVU acb:= AttachedCommandBehavior. 코드는 답변의 첫 번째 링크에서 찾을 수 있습니다
Rachel

나는 그것을 시도하고 클래스 CommandBehaviorBinding 라인 99에서 null 포인터 예외를 얻었습니다. 변수 "전략"은 null입니다. 뭐가 문제 야?
etwas77

13

Blend SDK 이벤트 트리거로이 작업을 수행하는 매우 쉽고 깔끔한 방법을 찾았습니다. 재사용 가능하고 코드 숨김이없는 깨끗한 MVVM.

이미 다음과 같은 것이있을 것입니다.

<Style x:Key="MyListStyle" TargetType="{x:Type ListViewItem}">

이제 아직 사용하지 않은 경우 다음과 같이 ListViewItem에 대한 ControlTemplate을 포함합니다.

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}" />
    </ControlTemplate>
  </Setter.Value>
 </Setter>

GridViewRowPresenter는 목록 행 요소를 구성하는 "내부"모든 요소의 시각적 루트가됩니다. 이제 여기에 트리거를 삽입하여 MouseDoubleClick 라우트 된 이벤트를 찾고 다음과 같이 InvokeCommandAction을 통해 명령을 호출 할 수 있습니다.

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>

GridRowPresenter (그리드로 시작하는 프로브) "위에"시각적 요소가있는 경우 여기에 트리거를 배치 할 수도 있습니다.

불행히도 MouseDoubleClick 이벤트는 모든 시각적 요소에서 생성되지는 않습니다 (예를 들어 FrameworkElements가 아닌 Controls에서 생성됨). 해결 방법은 EventTrigger에서 클래스를 파생시키고 ClickCount가 2 인 MouseButtonEventArgs를 찾는 것입니다. 이렇게하면 모든 비 MouseButtonEvents와 ClickCount! = 2 인 모든 MoseButtonEvents를 효과적으로 필터링합니다.

class DoubleClickEventTrigger : EventTrigger
{
    protected override void OnEvent(EventArgs eventArgs)
    {
        var e = eventArgs as MouseButtonEventArgs;
        if (e == null)
        {
            return;
        }
        if (e.ClickCount == 2)
        {
            base.OnEvent(eventArgs);
        }
    }
}

이제 다음과 같이 작성할 수 있습니다 ( 'h'는 위의 도우미 클래스의 네임 스페이스입니다).

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <h:DoubleClickEventTrigger EventName="MouseDown">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </h:DoubleClickEventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>

GridViewRowPresenter에 직접 트리거를두면 문제가있을 수 있음을 알았습니다. 열 사이의 빈 공간은 아마도 마우스 이벤트를 전혀받지 못할 것입니다 (아마도 해결 방법은 정렬 스트레치로 스타일을 지정하는 것입니다).
Gunter

이 경우 GridViewRowPresenter 주위에 빈 그리드를 배치하고 거기에 트리거를 배치하는 것이 좋습니다. 이것은 작동하는 것 같습니다.
Gunter

1
이와 같이 템플릿을 바꾸면 ListViewItem의 기본 스타일이 손실됩니다. 어쨌든 심하게 사용자 정의 된 스타일을 사용하고 있었기 때문에 내가 작업하고 있던 응용 프로그램에는 중요하지 않았습니다.
Gunter

6

이 토론은 1 년이 지났지 만 .NET 4에서이 솔루션에 대한 생각이 있습니까? 나는 MVVM의 요점이 파일 뒤에있는 코드를 제거하는 것이 아니라는 데 절대적으로 동의합니다. 나는 또한 무언가가 복잡하다고해서 그것이 더 낫다는 의미는 아니라는 것을 매우 강하게 느낍니다. 다음은 내가 코드에 넣은 내용입니다.

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }

12
viewmodel에는 도메인에서 수행 할 수있는 작업을 나타내는 이름이 있어야합니다. 도메인에서 "ButtonClick"작업은 무엇입니까? ViewModel은보기에 대한 도우미가 아닌보기 친화적 인 컨텍스트에서 도메인의 논리를 나타냅니다. 따라서 ButtonClick은 viewmodel에 있어서는 안되며 viewModel.DeleteSelectedCustomer 또는이 작업이 실제로 대신 나타내는 모든 것을 사용하십시오.
Marius

4

Caliburn 의 Action 기능을 사용하여 이벤트를 ViewModel의 메서드에 매핑 할 수 있습니다 . 에 ItemActivated메서드 가 있다고 가정하면 ViewModel해당 XAML은 다음과 같습니다.

<ListView x:Name="list" 
   Message.Attach="[Event MouseDoubleClick] = [Action ItemActivated(list.SelectedItem)]" >

자세한 내용은 Caliburn의 문서 및 샘플을 검토 할 수 있습니다.


4

보기를 만들 때 명령을 연결하는 것이 더 간단하다는 것을 알았습니다.

var r = new MyView();
r.MouseDoubleClick += (s, ev) => ViewModel.MyCommand.Execute(null);
BindAndShow(r, ViewModel);

제 경우에는 BindAndShow다음과 같습니다 (updatecontrols + avalondock).

private void BindAndShow(DockableContent view, object viewModel)
{
    view.DataContext = ForView.Wrap(viewModel);
    view.ShowAsDocument(dockManager);
    view.Focus();
}

접근 방식은 새로운 뷰를 여는 방법에 관계없이 작동해야합니다.


이것이 XAML에서만 작동하도록 시도하는 것이 아니라 가장 간단한 솔루션 인 것 같습니다.
Mas

1

InuptBindingsrushui 의 솔루션을 보았지만 배경을 투명으로 설정 한 후에도 텍스트가없는 ListViewItem 영역을 칠 수 없었기 때문에 다른 템플릿을 사용하여 해결했습니다.

이 템플릿은 ListViewItem이 선택되고 활성화 된 경우를위한 것입니다.

<ControlTemplate x:Key="SelectedActiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="LightBlue" HorizontalAlignment="Stretch">
   <!-- Bind the double click to a command in the parent view model -->
      <Border.InputBindings>
         <MouseBinding Gesture="LeftDoubleClick" 
                       Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ItemSelectedCommand}"
                       CommandParameter="{Binding}" />
      </Border.InputBindings>
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

이 템플릿은 ListViewItem이 선택되고 비활성 상태 일 때 사용됩니다.

<ControlTemplate x:Key="SelectedInactiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="Lavender" HorizontalAlignment="Stretch">
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

이것은 ListViewItem에 사용되는 기본 스타일입니다.

<Style TargetType="{x:Type ListViewItem}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate>
            <Border HorizontalAlignment="Stretch">
               <TextBlock Text="{Binding TextToShow}" />
            </Border>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Style.Triggers>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="True" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedActiveTemplate}" />
      </MultiTrigger>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="False" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedInactiveTemplate}" />
      </MultiTrigger>
   </Style.Triggers>
</Style>

내가 싫어하는 것은 TextBlock과 해당 텍스트 바인딩의 반복입니다. 한 위치에서이를 선언 할 수 있는지 모르겠습니다.

나는 이것이 누군가를 돕기를 바랍니다!


이것은 훌륭한 솔루션이며 비슷한 솔루션을 사용하지만 실제로는 하나의 컨트롤 템플릿 만 필요합니다. 사용자가을 두 번 클릭하려는 listviewitem경우 이미 선택되어 있는지 여부는 상관하지 않습니다. 또한 listview스타일 에 맞게 하이라이트 효과를 조정해야 할 수도 있습니다 . 찬성.
David Bentley

1

상호 작용 라이브러리를 사용하여 .Net 4.7 프레임 워크에서이 기능을 만드는 데 성공했습니다. 먼저 XAML 파일에서 네임 스페이스를 선언해야합니다.

xmlns : i = "http://schemas.microsoft.com/expression/2010/interactivity"

그런 다음 아래와 같이 ListView 내에서 각각의 InvokeCommandAction으로 이벤트 트리거를 설정합니다.

전망:

<ListView x:Name="lv" IsSynchronizedWithCurrentItem="True" 
          ItemsSource="{Binding Path=AppsSource}"  >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction CommandParameter="{Binding ElementName=lv, Path=SelectedItem}"
                                   Command="{Binding OnOpenLinkCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Developed By" DisplayMemberBinding="{Binding DevelopedBy}" />
        </GridView>
    </ListView.View>
</ListView>

위의 코드를 수정하면 ViewModel에서 더블 클릭 이벤트가 작동하도록하는 데 충분해야합니다. 그러나 전체 아이디어를 얻을 수 있도록 예제에서 Model 및 View Model 클래스를 추가했습니다.

모델:

public class ApplicationModel
{
    public string Name { get; set; }

    public string DevelopedBy { get; set; }
}

모델보기 :

public class AppListVM : BaseVM
{
        public AppListVM()
        {
            _onOpenLinkCommand = new DelegateCommand(OnOpenLink);
            _appsSource = new ObservableCollection<ApplicationModel>();
            _appsSource.Add(new ApplicationModel("TEST", "Luis"));
            _appsSource.Add(new ApplicationModel("PROD", "Laurent"));
        }

        private ObservableCollection<ApplicationModel> _appsSource = null;

        public ObservableCollection<ApplicationModel> AppsSource
        {
            get => _appsSource;
            set => SetProperty(ref _appsSource, value, nameof(AppsSource));
        }

        private readonly DelegateCommand _onOpenLinkCommand = null;

        public ICommand OnOpenLinkCommand => _onOpenLinkCommand;

        private void OnOpenLink(object commandParameter)
        {
            ApplicationModel app = commandParameter as ApplicationModel;

            if (app != null)
            {
                //Your code here
            }
        }
}

DelegateCommand 클래스 의 구현이 필요한 경우 .


0

다음은 ListBoxListView.

public class ItemDoubleClickBehavior : Behavior<ListBox>
{
    #region Properties
    MouseButtonEventHandler Handler;
    #endregion

    #region Methods

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.PreviewMouseDoubleClick += Handler = (s, e) =>
        {
            e.Handled = true;
            if (!(e.OriginalSource is DependencyObject source)) return;

            ListBoxItem sourceItem = source is ListBoxItem ? (ListBoxItem)source : 
                source.FindParent<ListBoxItem>();

            if (sourceItem == null) return;

            foreach (var binding in AssociatedObject.InputBindings.OfType<MouseBinding>())
            {
                if (binding.MouseAction != MouseAction.LeftDoubleClick) continue;

                ICommand command = binding.Command;
                object parameter = binding.CommandParameter;

                if (command.CanExecute(parameter))
                    command.Execute(parameter);
            }
        };
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewMouseDoubleClick -= Handler;
    }

    #endregion
}

다음은 부모를 찾는 데 사용되는 확장 클래스입니다.

public static class UIHelper
{
    public static T FindParent<T>(this DependencyObject child, bool debug = false) where T : DependencyObject
    {
        DependencyObject parentObject = VisualTreeHelper.GetParent(child);

        //we've reached the end of the tree
        if (parentObject == null) return null;

        //check if the parent matches the type we're looking for
        if (parentObject is T parent)
            return parent;
        else
            return FindParent<T>(parentObject);
    }
}

용법:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:coreBehaviors="{{Your Behavior Namespace}}"


<ListView AllowDrop="True" ItemsSource="{Binding Data}">
    <i:Interaction.Behaviors>
       <coreBehaviors:ItemDoubleClickBehavior/>
    </i:Interaction.Behaviors>

    <ListBox.InputBindings>
       <MouseBinding MouseAction="LeftDoubleClick" Command="{Binding YourCommand}"/>
    </ListBox.InputBindings>
</ListView>
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.