MVVMを使用してWPFリストビューアイテムからダブルクリックイベントを発生させる


102

MVVMを使用するWPFアプリケーションで、リストビューアイテムを持つユーザーコントロールがあります。実行時には、データバインディングを使用して、リストビューをオブジェクトのコレクションで埋めます。

ダブルクリックイベントをリストビューのアイテムにアタッチして、リストビューのアイテムがダブルクリックされると、ビューモデルの対応するイベントが発生し、クリックされたアイテムへの参照を持つようにする正しい方法は何ですか?

どのようにすれば、クリーンなMVVMの方法で実行できますか。つまり、ビューの背後にコードがありません。

回答:


76

コードビハインドは悪いことではありません。残念ながら、WPFコミュニティの多くの人がこれを間違っています。

MVVMは、背後にあるコードを排除するためのパターンではありません。それは、ロジックパーツ(ワークフロー)からビューパーツ(外観、アニメーションなど)を分離することです。さらに、ロジック部分を単体テストすることができます。

データバインディングがすべての解決策ではないため、コードを背後で記述しなければならないシナリオは十分にわかっています。あなたのシナリオでは、コードビハインドファイルでDoubleClickイベントを処理し、この呼び出しをViewModelに委任します。

背後でコードを使用し、なおかつMVVM分離を実現するサンプルアプリケーションは、次の場所にあります。

WPFアプリケーションフレームワーク(WAF) - http://waf.codeplex.com


5
よく言って、ダブルクリックするためだけにそのすべてのコードと追加のDLLを使用することを拒否します!
Eduardo Molteni

4
これだけのバインディングの使用は私に本当の頭痛を与えています。それは、片方の腕、片方の目をアイパッチで、片足で立ってコードを書くように要求されるようなものです。ダブルクリックは簡単なはずですが、このすべての追加コードがそれだけの価値があるかどうかはわかりません。
10

1
完全にあなたに同意することはできません。「コードビハインドは悪くない」と言ったら、それについて質問があります。ボタンのクリックイベントを委任するのではなく、代わりに(Commandプロパティを使用して)バインディングを使用するのはなぜですか?
Nam G VU

21
@Nam Gi VU:WPFコントロールでサポートされている場合は、常にコマンドバインディングを使用します。コマンドバインディングは、 'Click'イベントをViewModelに中継するだけではありません(CanExecuteなど)。ただし、コマンドは最も一般的なシナリオでのみ使用できます。他のシナリオでは、コードビハインドファイルを使用でき、UIに関連しない問題をViewModelまたはモデルに委任します。
jbe

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
領域全体では機能しないようです。たとえば、これをドックパネルで実行すると、ドックパネル内に何かがある場合にのみ機能します(テキストブロック、画像など)が、空白スペースは機能しません。
スティーブンドリュー

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の非常に優れた実装を備えています。これらを使用して、ListViewのItemContainerStyleプロパティにスタイルを割り当て、各ListViewItemにコマンドを設定できます。

ここでは、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 2014

13

Blend SDK Eventトリガーでこれを行う非常に簡単でクリーンな方法を見つけました。クリーンな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イベントはすべてのビジュアル要素から生成されるわけではありません(たとえば、Controlsからではなく、FrameworkElementsからではありません)。回避策は、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に直接配置すると、問題が発生する可能性があることがわかりました。カラム間の空のスペースは、おそらくマウスイベントをまったく受け取りません(おそらく、回避策は、整列ストレッチでスタイルを設定することです)。
ガンター

この場合、GridViewRowPresenterの周りに空のグリッドを配置し、そこにトリガーを配置する方がおそらく良いでしょう。これは動作するようです。
ガンター2011年

1
このようにテンプレートを置き換えると、ListViewItemのデフォルトスタイルが失われることに注意してください。いずれにしても、大幅にカスタマイズされたスタイリングを使用していたので、私が取り組んでいたアプリケーションには関係ありませんでした。
ガンター

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.DeleteSelectedCustomerまたはこのアクションが実際に表すものを使用してください。
Marius

4

Caliburnのアクション機能を使用して、イベントを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でのみ機能するようにするのではなく、最も簡単なソリューションのようです。
マス

1

InuptBindingsでrushui からの解決策を確認しましたが、背景が透明に設定された後でも、テキストがない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とそのテキストバインディングの繰り返しです。IIがそれを1つの場所で宣言できるかどうかはわかりません。

これが誰かに役立つことを願っています!


これは優れたソリューションであり、私は同様のソリューションを使用していますが、実際に必要なコントロールテンプレートは1つだけです。ユーザーがをダブルクリックするlistviewitem場合、すでに選択されているかどうかは気にしないでしょう。また、listviewスタイルに合わせてハイライト効果を微調整する必要がある場合があることに注意することも重要です。賛成票。
David Bentley、2017

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

ここでは、両方で行われていることを取得行動だListBoxとはListView

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.