Disparar un evento de doble clic desde un elemento ListView de WPF usando MVVM

102

En una aplicación WPF que usa MVVM, tengo un control de usuario con un elemento de vista de lista. En tiempo de ejecución, utilizará el enlace de datos para llenar la vista de lista con una colección de objetos.

¿Cuál es la forma correcta de adjuntar un evento de doble clic a los elementos de la vista de lista para que cuando se haga doble clic en un elemento de la vista de lista, se active un evento correspondiente en el modelo de vista y tenga una referencia al elemento en el que se hizo clic?

¿Cómo se puede hacer de una manera MVVM limpia, es decir, sin código detrás en la Vista?

Emad Gabriel
fuente

Respuestas:

76

Por favor, el código subyacente no es nada malo. Desafortunadamente, muchas personas en la comunidad de WPF se equivocaron.

MVVM no es un patrón para eliminar el código subyacente. Es separar la parte de la vista (apariencia, animaciones, etc.) de la parte lógica (flujo de trabajo). Además, puede realizar una prueba unitaria de la parte lógica.

Conozco suficientes escenarios en los que tienes que escribir código detrás porque el enlace de datos no es una solución para todo. En su escenario, manejaría el evento de DoubleClick en el código detrás del archivo y delegaría esta llamada al ViewModel.

Las aplicaciones de muestra que usan código subyacente y aún cumplen con la separación MVVM se pueden encontrar aquí:

Marco de aplicación WPF (WAF): http://waf.codeplex.com

jbe
fuente
5
Bien dicho, me niego a usar todo ese código y una DLL adicional solo para hacer doble clic.
Eduardo Molteni
4
Este único uso de la vinculación me está dando un verdadero dolor de cabeza. Es como pedirle que codifique con 1 brazo, 1 ojo en un parche en el ojo y pararse en 1 pierna. El doble clic debería ser simple, y no veo cómo vale la pena todo este código adicional.
Echiban
1
Me temo que no estoy totalmente de acuerdo contigo. Si dice 'el código subyacente no es malo', entonces tengo una pregunta al respecto: ¿Por qué no delegamos el evento de clic para el botón pero a menudo usamos el enlace (usando la propiedad Command) en su lugar?
Nam G VU
21
@Nam Gi VU: siempre preferiría un enlace de comandos cuando es compatible con el control WPF. Un enlace de comandos hace más que simplemente transmitir el evento 'Click' al ViewModel (por ejemplo, CanExecute). Pero los comandos solo están disponibles para los escenarios más comunes. Para otros escenarios, podemos usar el archivo de código subyacente y allí delegamos las preocupaciones no relacionadas con la interfaz de usuario al ViewModel o al Model.
jbe
2
¡Ahora te entiendo más! ¡Buena discusión contigo!
Nam G VU
73

Puedo hacer que esto funcione con .NET 4.5. Parece sencillo y no se necesita ningún tercero o código detrás.

<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
fuente
2
No parece funcionar para toda el área, por ejemplo, hago esto en un panel de acoplamiento y solo funciona donde hay algo dentro del panel de acoplamiento (por ejemplo, bloque de texto, imagen) pero no el espacio en blanco.
Stephen Drew
3
De acuerdo, esta vieja castaña de nuevo ... necesito configurar el fondo en transparente para recibir eventos del mouse, según stackoverflow.com/questions/7991314/…
Stephen Drew
6
Estaba rascándome la cabeza tratando de averiguar por qué funcionaba para todos ustedes y no para mí. De repente me di cuenta de que dentro del contexto de la plantilla de elementos, el contexto de datos es el elemento actual de la fuente de elementos y no el modelo de vista de la ventana principal. Así que utilicé lo siguiente para que funcionara <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> En mi caso, EditBandCommand es el comando en el modelo de vista de la página no en la entidad vinculada.
naskew
naskew tenía la salsa secreta que necesitaba con MVVM Light, obteniendo un parámetro de comando que es el objeto modelo en el listboxitem al que se hace doble clic, y el contexto de datos de la ventana se establece en el modelo de vista que expone el comando: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5
Solo quiero agregar que InputBindingsestán disponibles en .NET 3.0 y no están disponibles en Silverlight.
Martin
44

Me gusta usar Comandos y Comportamientos de comandos adjuntos . Marlon Grech tiene una muy buena implementación de los Comportamientos de comandos adjuntos. Usando estos, podríamos asignar un estilo a la propiedad ItemContainerStyle de ListView que establecerá el comando para cada ListViewItem.

Aquí configuramos el comando para que se active en el evento MouseDoubleClick, y CommandParameter, será el objeto de datos en el que hacemos clic. Aquí estoy viajando por el árbol visual para obtener el comando que estoy usando, pero también podría crear comandos para toda la aplicación con la misma facilidad.

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

Para los comandos, puede implementar un ICommand directamente o usar algunos de los ayudantes como los que vienen en MVVM Toolkit .

rmoore
fuente
1
+1 He descubierto que esta es mi solución preferida cuando trabajo con la Guía de aplicaciones compuestas para WPF (Prism).
Travis Heseman
1
¿Qué significa el espacio de nombres 'acb:' en el ejemplo de código anterior?
Nam G VU
@NamGiVU acb:= AttachedCommandBehavior. El código se puede encontrar en el primer enlace de la respuesta
Rachel
Lo intenté y obtuve una excepción de puntero nulo de la clase CommandBehaviorBinding línea 99. La variable "estrategia" es nula. que esta mal
etwas77
13

He encontrado una forma muy fácil y limpia de hacer esto con los activadores de eventos de Blend SDK. MVVM limpio, reutilizable y sin código subyacente.

Probablemente ya tengas algo como esto:

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

Ahora incluya un ControlTemplate para ListViewItem como este si aún no usa uno:

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

GridViewRowPresenter será la raíz visual de todos los elementos "dentro" que componen un elemento de fila de lista. Ahora podríamos insertar un disparador allí para buscar eventos enrutados MouseDoubleClick y llamar a un comando a través de InvokeCommandAction como este:

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

Si tiene elementos visuales "encima" del GridRowPresenter (probablemente comenzando con una cuadrícula), también puede colocar el Trigger allí.

Desafortunadamente, los eventos MouseDoubleClick no se generan a partir de todos los elementos visuales (son de Controls, pero no de FrameworkElements, por ejemplo). Una solución alternativa es derivar una clase de EventTrigger y buscar MouseButtonEventArgs con un ClickCount de 2. Esto filtra eficazmente todos los eventos que no son MouseButtonEvents y todos los MoseButtonEvents con un 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);
        }
    }
}

Ahora podemos escribir esto ('h' es el espacio de nombres de la clase auxiliar anterior):

<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
fuente
Como descubrí, si coloca el Trigger directamente en GridViewRowPresenter, podría haber un problema. Los espacios vacíos entre las columnas probablemente no reciben eventos del mouse en absoluto (probablemente una solución alternativa sería diseñarlos con estiramiento de alineación).
Gunter
En este caso, probablemente sea mejor colocar una cuadrícula vacía alrededor de GridViewRowPresenter y colocar el disparador allí. Esto parece funcionar.
Gunter
1
Tenga en cuenta que pierde el estilo predeterminado para ListViewItem si reemplaza la plantilla de esta manera. No importaba para la aplicación en la que estaba trabajando, ya que de todos modos estaba usando un estilo muy personalizado.
Gunter
6

Me doy cuenta de que esta discusión tiene un año, pero con .NET 4, ¿hay alguna idea sobre esta solución? Estoy absolutamente de acuerdo en que el objetivo de MVVM NO es eliminar un código detrás de un archivo. También creo firmemente que el hecho de que algo sea complicado no significa que sea mejor. Esto es lo que puse en el código detrás:

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }
Aaron
fuente
12
El modelo de vista debe tener nombres que representen las acciones que puede realizar en su dominio. ¿Qué es una acción "ButtonClick" en su dominio? ViewModel representa la lógica del dominio en un contexto amigable para la vista, no es solo un ayudante para la vista. Entonces: ButtonClick nunca debería estar en el modelo de vista, use viewModel.DeleteSelectedCustomer o lo que sea que esta acción realmente represente en su lugar.
Marius
4

Puede utilizar la función Acción de Caliburn para asignar eventos a métodos en su ViewModel. Suponiendo que tiene un ItemActivatedmétodo en su ViewModel, el XAML correspondiente se vería así:

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

Para obtener más detalles, puede examinar la documentación y las muestras de Caliburn.

idursun
fuente
4

Me resulta más sencillo vincular el comando cuando se crea la vista:

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

En mi caso se BindAndShowve así (updatecontrols + avalondock):

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

Aunque el enfoque debería funcionar con cualquier método que tenga para abrir nuevas vistas.

Timothy Pratley
fuente
Me parece que esta es la solución más simple, en lugar de intentar que funcione solo en XAML.
Mas
1

Vi la solución de Rushui con InuptBindings, pero todavía no pude acceder al área de ListViewItem donde no había texto, incluso después de configurar el fondo en transparente, así que lo resolví usando diferentes plantillas.

Esta plantilla es para cuando se ha seleccionado ListViewItem y está activo:

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

Esta plantilla es para cuando se ha seleccionado ListViewItem y está inactivo:

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

Este es el estilo predeterminado utilizado para 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>

Lo que no me gusta es la repetición del TextBlock y su enlace de texto, no sé si puedo evitar declarar eso en una sola ubicación.

¡Espero que esto ayude a alguien!

user3235445
fuente
Esta es una gran solución y yo uso una similar, pero realmente solo necesitas una plantilla de control. Si un usuario va a hacer doble clic en un listviewitem, probablemente no le importe si ya está seleccionado o no. También es importante tener en cuenta que es posible que el efecto de resaltado también deba modificarse para que coincida con el listviewestilo. Votado a favor.
David Bentley
1

Logré hacer esta funcionalidad con el marco .Net 4.7 utilizando la biblioteca de interactividad, en primer lugar, asegúrese de declarar el espacio de nombres en el archivo XAML

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

Luego, configure el Activador de eventos con su respectiva InvokeCommandAction dentro de ListView como se muestra a continuación.

Ver:

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

Adaptar el código anterior debería ser suficiente para que el evento de doble clic funcione en su ViewModel, sin embargo, le agregué la clase Model y View Model de mi ejemplo para que pueda tener la idea completa.

Modelo:

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

    public string DevelopedBy { get; set; }
}

Ver modelo:

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

En caso de que necesite la implementación de la clase DelegateCommand .

luis_laurent
fuente
0

Aquí hay un comportamiento que hace eso en ambos ListBoxy 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
}

Aquí está la clase de extensión utilizada para encontrar el padre.

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

Uso:

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>
Príncipe Owen
fuente