Uruchamianie zdarzenia dwukrotnego kliknięcia z elementu WPF ListView przy użyciu MVVM

102

W aplikacji WPF korzystającej z MVVM mam kontrolę użytkownika z elementem widoku listy. W czasie wykonywania użyje wiązania danych, aby wypełnić widok listy kolekcją obiektów.

Jaki jest prawidłowy sposób dołączenia zdarzenia dwukrotnego kliknięcia do elementów w widoku listy, tak aby po dwukrotnym kliknięciu elementu w widoku listy odpowiadające mu zdarzenie w modelu widoku było uruchamiane i miało odniesienie do klikniętego elementu?

Jak można to zrobić w czysty sposób MVVM, tj. Bez kodu w widoku?

Emad Gabriel
źródło

Odpowiedzi:

76

Proszę, kod znajdujący się za nim nie jest wcale złą rzeczą. Niestety, sporo osób w społeczności WPF pomyliło się.

MVVM nie jest wzorcem eliminującym kod. Ma na celu oddzielenie części widoku (wygląd, animacje itp.) Od części logicznej (workflow). Ponadto możesz przetestować jednostkę logiczną.

Znam wystarczająco dużo scenariuszy, w których musisz pisać kod, ponieważ powiązanie danych nie jest rozwiązaniem dla wszystkiego. W twoim scenariuszu obsłużyłem zdarzenie DoubleClick w kodzie związanym z plikiem i delegowałem to wywołanie do ViewModel.

Przykładowe aplikacje, które używają kodu stojącego za i nadal spełniają separację MVVM, można znaleźć tutaj:

Struktura aplikacji WPF (WAF) - http://waf.codeplex.com

jbe
źródło
5
Dobrze powiedziane, odmawiam użycia całego tego kodu i dodatkowej biblioteki DLL, aby wykonać dwukrotne kliknięcie!
Eduardo Molteni
4
To tylko użycie Wiązania przyprawia mnie o prawdziwy ból głowy. To tak, jakby ktoś poprosił o kodowanie jedną ręką, jednym okiem na opasce na oku i stanie na jednej nodze. Podwójne kliknięcie powinno być proste i nie widzę, jak ten cały dodatkowy kod jest tego wart.
Echiban
1
Obawiam się, że nie do końca się z tobą zgadzam. Jeśli powiesz, że „kod za nim nie jest zły”, mam pytanie na ten temat: Dlaczego nie delegujemy zdarzenia kliknięcia dla przycisku, ale często zamiast tego używamy powiązania (przy użyciu właściwości Command)?
Nam G VU
21
@Nam Gi VU: Zawsze wolałbym powiązanie polecenia, gdy jest obsługiwane przez formant WPF. Powiązanie polecenia nie tylko przekazuje zdarzenie „Click” do ViewModel (np. CanExecute). Ale polecenia są dostępne tylko dla najbardziej typowych scenariuszy. W przypadku innych scenariuszy możemy użyć pliku związanego z kodem i tam delegujemy problemy niezwiązane z interfejsem użytkownika do ViewModel lub Model.
jbe
2
Teraz rozumiem cię bardziej! Niezła dyskusja z tobą!
Nam G VU,
73

Jestem w stanie zmusić to do pracy z .NET 4.5. Wydaje się proste i nie są potrzebne żadne strony trzecie ani kod.

<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
źródło
2
Wydaje się, że nie działa dla całego obszaru, np. Robię to na panelu dokowania i działa tylko wtedy, gdy jest coś w panelu dokowania (np. Blok tekstu, obraz), ale nie pusta przestrzeń.
Stephen Drew
3
OK - znowu ten stary kasztan ... muszę ustawić tło na przezroczyste, aby odbierać zdarzenia myszy, zgodnie ze stackoverflow.com/questions/7991314/ ...
Stephen Drew
6
Drapałem się po głowie, próbując zrozumieć, dlaczego to działa dla was wszystkich, a nie dla mnie. Nagle zdałem sobie sprawę, że w kontekście szablonu elementu kontekstem danych jest bieżący element ze źródła items, a nie model widoku głównego okna. Więc użyłem poniższego, aby to działało <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> W moim przypadku EditBandCommand to polecenie w modelu widoku strony nie dotyczy powiązanej jednostki.
naskew
naskew miał tajny sos, którego potrzebowałem w MVVM Light, uzyskując parametr polecenia będący obiektem modelu w dwukrotnym kliknięciu elementu listboxitem, a kontekst danych okna jest ustawiony na model widoku, który ujawnia polecenie: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5,
Chcę tylko dodać, że InputBindingssą dostępne w .NET 3.0 i nie są dostępne w Silverlight.
Martin
44

Lubię korzystać z zachowań i poleceń dołączonych poleceń. Marlon Grech ma bardzo dobrą implementację zachowań Attached Command. Korzystając z nich, możemy następnie przypisać styl do właściwości ItemContainerStyle elementu ListView, która ustawi polecenie dla każdego ListViewItem.

Tutaj ustawiamy polecenie, które ma być uruchamiane po zdarzeniu MouseDoubleClick, a CommandParameter będzie obiektem danych, na który klikamy. Tutaj podróżuję po drzewie wizualnym, aby uzyskać polecenie, którego używam, ale równie łatwo możesz utworzyć polecenia dotyczące całej aplikacji.

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

W przypadku poleceń możesz zaimplementować ICommand bezpośrednio lub użyć niektórych pomocników, takich jak te, które znajdują się w zestawie narzędzi MVVM .

rmoore
źródło
1
+1 Okazało się, że jest to preferowane przeze mnie rozwiązanie podczas pracy z wytycznymi dotyczącymi aplikacji złożonych dla WPF (pryzmat).
Travis Heseman
1
Co oznacza przestrzeń nazw „acb:” w powyższym przykładzie kodu?
Nam G VU
@NamGiVU acb:= AttachedCommandBehavior. Kod można znaleźć w pierwszym linku w odpowiedzi
Rachel
Próbowałem właśnie tego i otrzymałem wyjątek wskaźnika zerowego z wiersza 99 CommandBehaviorBinding klasy. Zmienna „strategia” ma wartość null. Co jest nie tak?
etwas77
13

Znalazłem bardzo łatwy i czysty sposób na zrobienie tego za pomocą wyzwalaczy zdarzeń Blend SDK. Czysty MVVM, wielokrotnego użytku i bez kodu źródłowego.

Prawdopodobnie masz już coś takiego:

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

Teraz dołącz ControlTemplate dla ListViewItem w ten sposób, jeśli jeszcze go nie używasz:

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

GridViewRowPresenter będzie wizualnym korzeniem wszystkich elementów „wewnątrz” tworzących element wiersza listy. Teraz możemy wstawić tam wyzwalacz, aby szukać zdarzeń kierowanych przez MouseDoubleClick i wywołać polecenie za pośrednictwem InvokeCommandAction w następujący sposób:

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

Jeśli masz elementy wizualne „nad” GridRowPresenter (prawdopodobnie zaczynając od siatki), możesz również umieścić tam wyzwalacz.

Niestety zdarzenia MouseDoubleClick nie są generowane z każdego elementu wizualnego (pochodzą z Controls, ale nie na przykład z FrameworkElements). Sposób obejścia problemu polega na wyprowadzeniu klasy z EventTrigger i wyszukaniu MouseButtonEventArgs z wartością ClickCount równą 2. To skutecznie odfiltrowuje wszystkie zdarzenia inne niż MouseButtonEvents i wszystkie zdarzenia MoseButtonEvents z wartością 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);
        }
    }
}

Teraz możemy napisać to ('h' to przestrzeń nazw powyższej klasy pomocniczej):

<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
źródło
Jak się dowiedziałem, jeśli umieścisz wyzwalacz bezpośrednio w GridViewRowPresenter, może wystąpić problem. Puste przestrzenie między kolumnami prawdopodobnie w ogóle nie odbierają zdarzeń myszy (prawdopodobnie rozwiązaniem byłoby nadanie im stylu z rozciągnięciem wyrównania).
Gunter
W takim przypadku prawdopodobnie lepiej jest umieścić pustą siatkę wokół GridViewRowPresenter i tam umieścić wyzwalacz. To wydaje się działać.
Gunter
1
Zauważ, że utracisz domyślny styl ListViewItem, jeśli zamienisz szablon w ten sposób. Nie miało to znaczenia dla aplikacji, nad którą pracowałem, ponieważ i tak używała mocno dostosowanej stylizacji.
Gunter
6

Zdaję sobie sprawę, że ta dyskusja ma już rok, ale czy w przypadku .NET 4 są jakieś przemyślenia na temat tego rozwiązania? Absolutnie zgadzam się, że celem MVVM NIE jest eliminacja kodu związanego z plikiem. Czuję też bardzo mocno, że to, że coś jest skomplikowane, nie oznacza, że ​​jest lepiej. Oto, co umieściłem w kodzie:

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }
Aaron
źródło
12
Viewmodel powinien mieć nazwy reprezentujące czynności, które możesz wykonywać w swojej domenie. Co to jest czynność „ButtonClick” w Twojej domenie? ViewModel reprezentuje logikę domeny w kontekście przyjaznym dla widoku, a nie tylko pomocnikiem widoku. Tak więc: ButtonClick nigdy nie powinien znajdować się w viewmodel, zamiast tego użyj viewModel.DeleteSelectedCustomer lub czegokolwiek, co ta akcja faktycznie reprezentuje.
Marius
4

Możesz użyć funkcji Action Caliburn do mapowania zdarzeń na metody w Twoim ViewModel. Zakładając, że masz ItemActivatedmetodę na swojej ViewModel, odpowiedni kod XAML wyglądałby następująco:

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

Aby uzyskać więcej informacji, zapoznaj się z dokumentacją i próbkami Caliburn.

idursun
źródło
4

Wydaje mi się, że łatwiej jest połączyć polecenie podczas tworzenia widoku:

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

W moim przypadku BindAndShowwygląda to tak (kontrola aktualizacji + avalondock):

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

Chociaż podejście powinno działać z dowolną metodą otwierania nowych widoków.

Timothy Pratley
źródło
Wydaje mi się, że jest to najprostsze rozwiązanie, zamiast próbować sprawić, by działało tylko w XAML.
Mas
1

Widziałem rozwiązanie z rushui z InuptBindings, ale nadal nie byłem w stanie trafić w obszar ListViewItem, w którym nie ma tekstu - nawet po ustawieniu tła na przezroczyste, więc rozwiązałem to za pomocą różnych szablonów.

Ten szablon jest przeznaczony dla sytuacji, gdy ListViewItem został wybrany i jest aktywny:

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

Ten szablon jest używany, gdy ListViewItem został wybrany i jest nieaktywny:

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

To jest domyślny styl używany dla 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>

To, co mi się nie podoba, to powtarzanie TextBlock i jego wiązanie tekstu, nie wiem, czy mogę obejść się, deklarując, że tylko w jednym miejscu.

Mam nadzieję, że to komuś pomoże!

user3235445
źródło
To świetne rozwiązanie i używam podobnego, ale tak naprawdę potrzebujesz tylko jednego szablonu kontrolnego. Jeśli użytkownik będzie klikał dwukrotnie listviewitem, prawdopodobnie nie obchodzi go, czy jest już zaznaczony, czy nie. Należy również zauważyć, że efekt podświetlenia może również wymagać dostosowania, aby pasował do listviewstylu. Głosowano za.
David Bentley
1

Udało mi się stworzyć tę funkcjonalność z frameworkiem .Net 4.7 przy użyciu biblioteki interaktywności, przede wszystkim upewnij się, że zadeklarowałem przestrzeń nazw w pliku XAML

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

Następnie ustaw wyzwalacz zdarzenia z jego odpowiednim InvokeCommandAction wewnątrz ListView, jak poniżej.

Widok:

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

Dostosowanie powyższego kodu powinno wystarczyć, aby zdarzenie dwukrotnego kliknięcia działało na Twoim ViewModel, jednak dodałem Ci klasę Model i View Model z mojego przykładu, abyś miał pełny pomysł.

Model:

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

    public string DevelopedBy { get; set; }
}

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

Na wypadek, gdybyś potrzebował implementacji klasy DelegateCommand .

luis_laurent
źródło
0

Oto zachowanie, które robi to na obu ListBoxi 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
}

Oto klasa rozszerzenia używana do znalezienia rodzica.

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

Stosowanie:

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>
Książę Owen
źródło