MVVM kullanarak bir WPF ListView öğesinden çift tıklama olayını tetikleme


102

MVVM kullanan bir WPF uygulamasında, listview öğesi olan bir kullanıcı denetimim var. Çalışma zamanında, liste görünümünü bir nesne koleksiyonuyla doldurmak için veri bağlamayı kullanır.

Liste görünümündeki öğelere çift tıklama olayı eklemenin doğru yolu nedir, böylece liste görünümündeki bir öğe çift tıklandığında, görünüm modelinde karşılık gelen bir olay tetiklenir ve tıklanan öğeye bir referansı olur?

Temiz bir MVVM yöntemiyle, yani Görünümde kod olmadan nasıl yapılabilir?

Yanıtlar:


76

Lütfen, arkasındaki kod kötü bir şey değildir. Ne yazık ki, WPF topluluğundaki pek çok kişi bunu yanlış anladı.

MVVM, arkasındaki kodu ortadan kaldıracak bir model değildir. Görünüm bölümünü (görünüm, animasyonlar, vb.) Mantık bölümünden (iş akışı) ayırmaktır. Ayrıca, mantık bölümünü birim test edebilirsiniz.

Veri bağlama her şeye bir çözüm olmadığı için arkasına kod yazmanız gereken yeterince senaryo biliyorum. Senaryonuzda, dosyanın arkasındaki kodda DoubleClick olayını idare eder ve bu çağrıyı ViewModel'e devrederdim.

Arkasındaki kodu kullanan ve yine de MVVM ayrımını gerçekleştiren örnek uygulamalar burada bulunabilir:

WPF Uygulama Çerçevesi (WAF) - http://waf.codeplex.com


5
İyi dedim, sadece çift tıklama yapmak için tüm bu kodu ve fazladan bir DLL'yi kullanmayı reddediyorum!
Eduardo Molteni

4
Bu sadece ciltleme işini kullanmak bana gerçek bir baş ağrısı veriyor. 1 kolla, 1 göz bandıyla ve 1 bacak üzerinde durarak kodlamanız isteniyor gibi. Çift tıklama basit olmalı ve tüm bu ek kodun buna değer olduğunu anlamıyorum.
Echiban

1
Korkarım sana tamamen katılmıyorum. Eğer 'arkasındaki kod kötü değil' diyorsan, bununla ilgili bir sorum var: Neden tıklama olayını düğme için yetkilendirmiyoruz ama bunun yerine sık sık bağlama kullanmıyoruz (Komut özelliğini kullanarak)?
Nam G VU

21
@Nam Gi VU: WPF Kontrolü tarafından desteklendiğinde her zaman bir Komut Bağlamayı tercih ederim. Bir Komut Bağlama, ViewModel'e (örn. CanExecute) 'Tıklama' olayını iletmekten daha fazlasını yapar. Ancak Komutlar yalnızca en yaygın senaryolar için kullanılabilir. Diğer senaryolar için arka plan kod dosyasını kullanabiliriz ve burada UI ile ilgili olmayan endişeleri ViewModel veya Model'e devrederiz.
jbe

2
Şimdi seni daha çok anlıyorum! Sizinle güzel tartışma!
Nam G VU

73

Bunu .NET 4.5 ile çalıştırabiliyorum. Basit görünüyor ve arkasında üçüncü taraf veya kod gerekli değil.

<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
Görünüşe göre tüm alan için işe yaramıyor, örneğin bunu bir dock panelinde yapıyorum ve sadece dock panelinde bir şey (örneğin metin bloğu, resim) olduğu, ancak boş alan olmadığı yerde çalışıyor.
Stephen Drew

3
Tamam - Bu yaşlı kestane yine ... Gerek gereğince, fare olayları almak saydam arka planı ayarlamak için stackoverflow.com/questions/7991314/...
Stephen Drew'u

6
Neden benim için değil de hepiniz için işe yaradığını anlamaya çalışırken kafamı kaşıyordum. Aniden, öğe şablonu bağlamında veri bağlamının ana pencerenin görünüm modeli değil, öğe kaynağındaki geçerli öğe olduğunu fark ettim. Bu yüzden onu çalıştırmak için aşağıdakileri kullandım <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}" /> Benim durumumda EditBandCommand sayfanın görünüm modelindeki komut bağlı varlık üzerinde değil.
naskew

naskew, MVVM Light ile ihtiyacım olan gizli sosa sahipti, çift tıklanan listboxitem'de model nesnesi olan bir komut parametresi elde etti ve pencerenin veri bağlamı, şu komutu açığa çıkaran görünüm modeline ayarlandı: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5

Sadece bu eklemek istediğiniz InputBindings.NET 3.0 temin edilebilir ve vardır değil Silverlight mevcuttur.
Martin

44

Ekli Komut Davranışları ve Komutları kullanmayı seviyorum . Marlon Grech , Ekli Komut Davranışları'nın çok iyi bir uygulamasına sahiptir. Bunları kullanarak, ListView'ın ItemContainerStyle özelliğine her ListViewItem için komutu ayarlayacak bir stil atayabiliriz .

Burada MouseDoubleClick olayında çalıştırılacak komutu belirledik ve CommandParameter, tıkladığımız veri nesnesi olacaktır. Burada kullandığım komutu almak için görsel ağaçta yukarı çıkıyorum, ancak aynı şekilde uygulama çapında komutlar da oluşturabilirsiniz.

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

Komutlar için, doğrudan bir ICommand uygulayabilir veya MVVM Toolkit ile gelenler gibi bazı yardımcıları kullanabilirsiniz .


1
+1 WPF (Prism) için Kompozit Uygulama Kılavuzu ile çalışırken bunu tercih ettiğim çözüm olarak buldum.
Travis Heseman

1
Yukarıdaki kod örneğinizde 'acb:' ad alanı ne anlama geliyor?
Nam G VU

@NamGiVU acb:= AttachedCommandBehavior. Kodu cevaptaki ilk bağlantıda bulabilirsiniz
Rachel

Ben sadece bunu denedim ve CommandBehaviorBinding satır 99'dan boş işaretçi istisnası alıyorum. "strateji" değişkeni boş. Sorun nedir?
etwas77

13

Blend SDK Event tetikleyicileri ile bunu yapmanın çok kolay ve temiz bir yolunu buldum. Temiz MVVM, yeniden kullanılabilir ve arka planda kod yok.

Muhtemelen zaten böyle bir şeye sahipsiniz:

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

Zaten kullanmıyorsanız şimdi ListViewItem için bunun gibi bir ControlTemplate ekleyin:

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

GridViewRowPresenter, bir liste satırı öğesini oluşturan "içindeki" tüm öğelerin görsel kökü olacaktır. Şimdi MouseDoubleClick yönlendirilmiş olayları aramak için oraya bir tetikleyici ekleyebilir ve InvokeCommandAction aracılığıyla şu şekilde bir komut çağırabiliriz:

<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'ın "üstünde" görsel öğeleriniz varsa (olasılıkla bir ızgarayla başlayarak) Tetikleyiciyi oraya da koyabilirsiniz.

Ne yazık ki MouseDoubleClick olayları her görsel öğeden oluşturulmaz (bunlar Denetimlerden, ancak örneğin FrameworkElements'ten değil). Bir geçici çözüm, EventTrigger'dan bir sınıf türetmek ve ClickCount 2 olan MouseButtonEventArgs'ı aramaktır. Bu, bir ClickCount! = 2 ile MouseButtonEvents olmayan ve tüm MoseButtonEvents etkin bir şekilde filtreler.

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

Şimdi bunu yazabiliriz ('h' yukarıdaki yardımcı sınıfın Ad Alanıdır):

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

Tetikleyiciyi doğrudan GridViewRowPresenter'a koyarsanız bir sorun olabilir. Sütunlar arasındaki boş alanlar muhtemelen fare olaylarını hiç almıyor (büyük olasılıkla geçici bir çözüm onları hizalama uzatması ile biçimlendirmek olabilir).
Gunter

Bu durumda, GridViewRowPresenter çevresine boş bir ızgara koymak ve tetiği oraya koymak muhtemelen daha iyidir. Bu işe yarıyor gibi görünüyor.
Gunter

1
Şablonu bu şekilde değiştirirseniz ListViewItem için varsayılan stili kaybedeceğinizi unutmayın. Zaten yoğun bir şekilde özelleştirilmiş bir stil kullandığı için üzerinde çalıştığım uygulama için önemli değildi.
Gunter

6

Bu tartışmanın bir yaşında olduğunun farkındayım, ancak .NET 4 ile bu çözüm hakkında herhangi bir düşünceniz var mı? MVVM'nin amacının dosyanın arkasındaki bir kodu ortadan kaldırmak OLMADIĞINA kesinlikle katılıyorum. Ayrıca bir şeyin karmaşık olmasının daha iyi olduğu anlamına gelmediğini de çok kuvvetli hissediyorum. İşte kodun arkasına koyduğum şey:

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

12
Viewmodel, etki alanınızda gerçekleştirebileceğiniz eylemleri temsil eden adlara sahip olmalıdır. Etki alanınızda "ButtonClick" eylemi nedir? ViewModel, etki alanının mantığını görünüm dostu bir bağlamda temsil eder, yalnızca görünüm için bir yardımcı değildir. Yani: ButtonClick hiçbir zaman görünüm modelinde olmamalıdır, viewModel.DeleteSelectedCustomer'ı veya bunun yerine bu eylemin gerçekte temsil ettiği şeyi kullanın.
Marius

4

ViewModel'inizdeki olayları yöntemlerle eşlemek için Caliburn'un Eylem özelliğini kullanabilirsiniz. ItemActivatedÜzerinde bir yönteminiz olduğunu varsayarsak ViewModel, karşılık gelen XAML şöyle görünür:

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

Daha fazla ayrıntı için Caliburn'un belgelerini ve örneklerini inceleyebilirsiniz.


4

Görünüm oluşturulduğunda komutu bağlamayı daha kolay buluyorum:

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

Benim durumumda BindAndShowşöyle görünüyor (updatecontrols + avalondock):

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

Yine de yaklaşım, yeni görüşler açmak için sahip olduğunuz yöntemle çalışmalıdır.


Bana öyle geliyor ki bu, yalnızca XAML'de çalışmasını sağlamaya çalışmaktan ziyade en basit çözüm.
Mas

1

Rushui'den çözümü gördüm ile ancak ListViewItem alanında metnin olmadığı alana hala ulaşamadım - arka planı şeffaf olarak ayarladıktan sonra bile, bu yüzden farklı şablonlar kullanarak çözdüm.

Bu şablon, ListViewItem seçildiğinde ve etkin olduğunda kullanılır:

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

Bu şablon, ListViewItem seçildiğinde ve etkin olmadığında kullanılır:

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

Bu, ListViewItem için kullanılan Varsayılan stildir:

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

Sevmediğim şey, TextBlock'un tekrarlanması ve metin bağlaması, II'nin bunu sadece tek bir yerde ilan ederek etrafta dolaşabileceğini bilmiyorum.

Umarım bu birine yardımcı olur!


Bu harika bir çözüm ve ben benzerini kullanıyorum, ancak gerçekten yalnızca bir kontrol şablonuna ihtiyacınız var. Bir kullanıcı a'ya çift tıklayacaksa listviewitem, zaten seçilmiş olup olmadığını muhtemelen umursamayacaktır. Ayrıca, vurgu efektinin listviewstille eşleşmesi için ince ayar yapılması gerekebileceğini de unutmamak önemlidir . Yukarı oy verildi.
David Bentley

1

Bu işlevselliği.

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

Ardından, ListView içindeki ilgili InvokeCommandAction ile Olay Tetikleyicisini aşağıdaki gibi ayarlayın.

Görünüm:

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

Yukarıdaki kodu uyarlamak, çift tıklama olayının ViewModel'inizde çalışması için yeterli olacaktır, ancak size tüm fikre sahip olabilmeniz için örneğimden Model ve Model Görüntüle sınıfını ekledim.

Model:

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

    public string DevelopedBy { get; set; }
}

Modeli Görüntüle:

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 sınıfının uygulanmasına ihtiyacınız olması durumunda .


0

İşte hem ListBoxve hem de bunu başaran bir davranış 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
}

Üst öğeyi bulmak için kullanılan uzantı sınıfı burada.

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

Kullanım:

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>
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.