Mengaktifkan peristiwa klik ganda dari item WPF ListView menggunakan MVVM

102

Dalam aplikasi WPF menggunakan MVVM, saya memiliki kontrol pengguna dengan item listview. Dalam waktu proses, ini akan menggunakan penyatuan data untuk mengisi tampilan daftar dengan kumpulan objek.

Apa cara yang benar untuk melampirkan peristiwa klik ganda ke item dalam tampilan daftar sehingga ketika item dalam tampilan daftar diklik dua kali, peristiwa yang sesuai dalam model tampilan diaktifkan dan memiliki referensi ke item yang diklik?

Bagaimana hal itu dapat dilakukan dengan cara MVVM yang bersih, yaitu tidak ada kode di belakang dalam Tampilan?

Emad Gabriel
sumber

Jawaban:

76

Tolong, kode di belakang bukanlah hal yang buruk sama sekali. Sayangnya, cukup banyak orang di komunitas WPF yang melakukan kesalahan ini.

MVVM bukanlah pola untuk menghilangkan kode di belakang. Ini untuk memisahkan bagian tampilan (tampilan, animasi, dll.) Dari bagian logika (alur kerja). Selanjutnya, Anda dapat menguji unit bagian logika.

Saya cukup tahu skenario di mana Anda harus menulis kode di belakang karena pengikatan data bukanlah solusi untuk semuanya. Dalam skenario Anda, saya akan menangani acara DoubleClick dalam kode di belakang file dan mendelegasikan panggilan ini ke ViewModel.

Contoh aplikasi yang menggunakan kode di belakang dan masih memenuhi pemisahan MVVM dapat ditemukan di sini:

Kerangka Aplikasi WPF (WAF) - http://waf.codeplex.com

jbe
sumber
5
Nah, saya menolak untuk menggunakan semua kode itu dan DLL tambahan hanya untuk melakukan klik dua kali!
Eduardo Molteni
4
Ini hanya menggunakan hal Mengikat membuat saya sakit kepala. Ini seperti diminta untuk membuat kode dengan 1 lengan, 1 mata pada penutup mata, dan berdiri dengan 1 kaki. Klik dua kali seharusnya sederhana, dan saya tidak melihat betapa semua kode tambahan ini sepadan.
Echiban
1
Saya khawatir saya tidak sepenuhnya setuju dengan Anda. Jika Anda mengatakan 'kode di belakang tidak buruk', maka saya memiliki pertanyaan tentang itu: Mengapa kita tidak mendelegasikan peristiwa klik untuk tombol tetapi sering menggunakan pengikatan (menggunakan properti Command) sebagai gantinya?
Nam G VU
21
@Nam Gi VU: Saya akan selalu lebih suka Command Binding jika didukung oleh Kontrol WPF. Pengikatan Perintah melakukan lebih dari sekadar menyampaikan acara 'Klik' ke ViewModel (misalnya CanExecute). Tapi Perintah hanya tersedia untuk skenario yang paling umum. Untuk skenario lain kita dapat menggunakan file di belakang kode dan di sana kita mendelegasikan masalah yang tidak terkait dengan UI ke ViewModel atau Model.
jbe
2
Sekarang saya lebih memahami Anda! Diskusi yang bagus dengan Anda!
Nam G VU
73

Saya bisa mendapatkan ini untuk bekerja dengan .NET 4.5. Tampak lurus ke depan dan tidak perlu pihak ketiga atau kode di belakang.

<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>
Rushui Guan
sumber
2
Sepertinya tidak berfungsi untuk seluruh area, misalnya saya melakukan ini pada panel dok dan hanya berfungsi di mana ada sesuatu di dalam panel dok (mis. Blok teks, gambar) tetapi bukan ruang kosong.
Stephen Drew
3
Oke - kastanye tua ini lagi ... perlu menyetel latar belakang menjadi transparan untuk menerima peristiwa mouse, sesuai stackoverflow.com/questions/7991314/…
Stephen Drew
6
Aku menggaruk-garuk kepalaku mencoba mencari tahu mengapa itu berhasil untuk kalian semua dan bukan untukku. Saya tiba-tiba menyadari bahwa dalam konteks template item, konteks datanya adalah item saat ini dari itemssource dan bukan model tampilan jendela utama. Jadi saya menggunakan yang berikut ini agar berfungsi <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> Dalam kasus saya, EditBandCommand adalah perintah pada model tampilan halaman bukan pada entitas terikat.
naskew
naskew memiliki saus rahasia yang saya butuhkan dengan MVVM Light, mendapatkan parameter perintah sebagai objek model di listboxitem yang diklik dua kali, dan konteks data jendela disetel ke model tampilan yang mengekspos perintah: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5
Hanya ingin menambahkan yang InputBindingstersedia dari .NET 3.0 dan tidak tersedia di Silverlight.
Martin
44

Saya suka menggunakan Perilaku dan Perintah Terlampir . Marlon Grech memiliki implementasi yang sangat baik dari Perilaku Perintah Terlampir. Dengan menggunakan ini, kita kemudian dapat menetapkan gaya ke properti ItemContainerStyle ListView yang akan menyetel perintah untuk setiap ListViewItem.

Di sini kami menetapkan perintah untuk diaktifkan pada acara MouseDoubleClick, dan CommandParameter, akan menjadi objek data yang kami klik. Di sini saya melakukan perjalanan ke atas pohon visual untuk mendapatkan perintah yang saya gunakan, tetapi Anda dapat dengan mudah membuat perintah luas aplikasi.

<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>

Untuk perintah, Anda dapat mengimplementasikan ICommand secara langsung, atau menggunakan beberapa pembantu seperti yang ada di MVVM Toolkit .

rmoore
sumber
1
+1 Saya telah menemukan ini sebagai solusi pilihan saya saat bekerja dengan Panduan Aplikasi Komposit untuk WPF (Prism).
Travis Heseman
1
Apa singkatan dari namespace 'acb:' dalam contoh kode Anda di atas?
Nam G VU
@NamGiVU acb:= AttachedCommandBehavior. Kode dapat ditemukan di tautan pertama dalam jawaban
Rachel
saya mencoba hanya itu dan mendapatkan pengecualian pointer nol dari baris CommandBehaviorBinding kelas 99. variabel "strategi" adalah null. apa yang salah?
etwas77
13

Saya telah menemukan cara yang sangat mudah dan bersih untuk melakukan ini dengan pemicu Blend SDK Event. Membersihkan MVVM, dapat digunakan kembali dan tidak ada kode di belakang.

Anda mungkin sudah memiliki sesuatu seperti ini:

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

Sekarang sertakan ControlTemplate untuk ListViewItem seperti ini jika Anda belum menggunakannya:

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

GridViewRowPresenter akan menjadi root visual dari semua elemen "di dalam" yang menyusun elemen baris daftar. Sekarang kita dapat memasukkan pemicu di sana untuk mencari peristiwa yang diarahkan MouseDoubleClick dan memanggil perintah melalui InvokeCommandAction seperti ini:

<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>

Jika Anda memiliki elemen visual "di atas" GridRowPresenter (mungkin dimulai dengan kisi) Anda juga dapat meletakkan Pemicu di sana.

Sayangnya event MouseDoubleClick tidak dibuat dari setiap elemen visual (mereka berasal dari Controls, tetapi tidak dari FrameworkElements misalnya). Solusinya adalah dengan mendapatkan kelas dari EventTrigger dan mencari MouseButtonEventArgs dengan ClickCount 2. Ini secara efektif menyaring semua non-MouseButtonEvents dan semua MoseButtonEvents dengan ClickCount! = 2.

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);
        }
    }
}

Sekarang kita bisa menulis ini ('h' adalah Namespace dari kelas helper di atas):

<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>
Gunter
sumber
Seperti yang saya ketahui jika Anda meletakkan Trigger langsung di GridViewRowPresenter, mungkin ada masalah. Ruang kosong di antara kolom mungkin tidak menerima peristiwa mouse sama sekali (mungkin solusinya adalah menatanya dengan peregangan keselarasan).
Gunter
Dalam hal ini mungkin lebih baik untuk meletakkan kotak kosong di sekitar GridViewRowPresenter dan meletakkan pemicunya di sana. Sepertinya ini berhasil.
Gunter
1
Perhatikan bahwa Anda kehilangan gaya default untuk ListViewItem jika Anda mengganti template seperti ini. Tidak masalah untuk aplikasi yang saya kerjakan karena itu menggunakan gaya yang sangat disesuaikan.
Gunter
6

Saya menyadari bahwa diskusi ini sudah berumur satu tahun, tetapi dengan .NET 4, apakah ada pemikiran tentang solusi ini? Saya sangat setuju bahwa tujuan MVVM BUKAN untuk menghilangkan kode di belakang file. Saya juga merasa sangat yakin bahwa hanya karena sesuatu itu rumit, tidak berarti itu lebih baik. Inilah yang saya masukkan di kode di belakang:

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }
Aaron
sumber
12
Anda harus viewmodel harus memiliki nama yang mewakili tindakan yang dapat Anda lakukan di domain Anda. Apa yang dimaksud dengan tindakan "ButtonClick" di domain Anda? ViewModel mewakili logika domain dalam konteks ramah-tampilan, ini bukan hanya penolong untuk tampilan. Jadi: ButtonClick tidak boleh ada di viewmodel, gunakan viewModel.DeleteSelectedCustomer atau apa pun yang diwakili oleh tindakan ini.
Marius
4

Anda dapat menggunakan fitur Tindakan Caliburn untuk memetakan peristiwa ke metode pada ViewModel Anda. Dengan asumsi Anda memiliki ItemActivatedmetode pada Anda ViewModel, maka XAML yang sesuai akan terlihat seperti:

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

Untuk detail lebih lanjut, Anda dapat memeriksa dokumentasi dan sampel Caliburn.

idursun
sumber
4

Saya merasa lebih mudah untuk menautkan perintah saat tampilan dibuat:

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

Dalam kasus saya BindAndShowterlihat seperti ini (updatecontrols + avalondock):

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

Meskipun pendekatan tersebut harus bekerja dengan metode apa pun yang Anda miliki untuk membuka tampilan baru.

Timothy Pratley
sumber
Menurut saya ini adalah solusi paling sederhana, daripada mencoba membuatnya berfungsi hanya di XAML.
Mas
1

Saya melihat solusi dari rushui dengan InuptBindings tetapi saya masih tidak dapat mencapai area ListViewItem di mana tidak ada teks - bahkan setelah mengatur latar belakang menjadi transparan, jadi saya menyelesaikannya dengan menggunakan templat yang berbeda.

Template ini untuk saat ListViewItem telah dipilih dan aktif:

<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>

Template ini untuk saat ListViewItem telah dipilih dan tidak aktif:

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

Ini adalah gaya Default yang digunakan untuk 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>

Yang tidak saya suka adalah pengulangan dari TextBlock dan pengikatan teksnya, saya tidak tahu Saya bisa berkeliling menyatakan itu hanya di satu lokasi.

Saya harap ini membantu seseorang!

pengguna3235445
sumber
Ini adalah solusi yang bagus dan saya menggunakan yang serupa, tetapi Anda benar-benar hanya membutuhkan satu template kontrol. Jika pengguna akan mengklik ganda a listviewitem, mereka mungkin tidak peduli apakah itu sudah dipilih atau belum. Juga penting untuk dicatat bahwa efek sorotan mungkin juga perlu disesuaikan agar sesuai dengan listviewgayanya. Dipilih.
David Bentley
1

Saya berhasil membuat fungsionalitas ini dengan framework .Net 4.7 dengan menggunakan pustaka interaktivitas, pertama-tama pastikan untuk mendeklarasikan namespace di file XAML

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

Kemudian atur Event Trigger dengan masing-masing InvokeCommandAction di dalam ListView seperti di bawah ini.

Melihat:

<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>

Mengadaptasi kode di atas seharusnya cukup untuk membuat acara klik ganda berfungsi pada ViewModel Anda, namun saya menambahkan Anda kelas Model dan Model Tampilan dari contoh saya sehingga Anda dapat memiliki gagasan lengkap.

Model:

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

    public string DevelopedBy { get; set; }
}

Lihat Model:

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
            }
        }
}

Jika Anda membutuhkan implementasi kelas DelegateCommand .

luis_laurent
sumber
0

Inilah perilaku yang dilakukan pada keduanya ListBoxdan 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
}

Berikut kelas ekstensi yang digunakan untuk menemukan induk.

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);
    }
}

Pemakaian:

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>
Pangeran Owen
sumber