Disparando um evento de clique duplo de um item WPF ListView usando MVVM

102

Em um aplicativo WPF usando MVVM, eu tenho um controle de usuário com um item de exibição de lista. Em tempo de execução, ele usará databinding para preencher o listview com uma coleção de objetos.

Qual é a maneira correta de anexar um evento de clique duplo aos itens na visualização em lista para que, quando um item na visualização em lista for clicado duas vezes, um evento correspondente no modelo de visualização seja disparado e tenha uma referência ao item clicado?

Como isso pode ser feito de uma forma MVVM limpa, ou seja, sem código atrás no View?

Emad Gabriel
fonte

Respostas:

76

Por favor, code behind não é uma coisa ruim. Infelizmente, muitas pessoas na comunidade WPF entenderam isso errado.

MVVM não é um padrão para eliminar o código por trás. É para separar a parte da visualização (aparência, animações, etc.) da parte lógica (fluxo de trabalho). Além disso, você pode testar a unidade da parte lógica.

Eu conheço cenários suficientes em que você precisa escrever código porque a vinculação de dados não é uma solução para tudo. Em seu cenário, eu lidaria com o evento da DoubleClick no arquivo code behind e delegaria esta chamada ao ViewModel.

Aplicativos de exemplo que usam code behind e ainda cumprem a separação MVVM podem ser encontrados aqui:

WPF Application Framework (WAF) - http://waf.codeplex.com

jbe
fonte
5
Bem dito, eu me recuso a usar todo esse código e uma DLL extra apenas para fazer um clique duplo!
Eduardo Molteni
4
Essa coisa de usar Binding está me dando uma verdadeira dor de cabeça. É como ser solicitado a codificar com 1 braço, 1 olho em um tapa-olho e apoiado em 1 perna. O clique duplo deve ser simples, e não vejo como vale a pena todo esse código adicional.
Echiban,
1
Receio não concordo totalmente com você. Se você disser 'code behind não é ruim', então tenho uma pergunta sobre isso: Por que não delegamos o evento click para o botão, mas geralmente usando a vinculação (usando a propriedade Command) em vez disso?
Nam G VU
21
@Nam Gi VU: Eu sempre preferiria um Command Binding quando for suportado pelo WPF Control. Um Command Binding faz mais do que apenas retransmitir o evento 'Click' para o ViewModel (por exemplo, CanExecute). Mas os comandos estão disponíveis apenas para os cenários mais comuns. Para outros cenários, podemos usar o arquivo code-behind e, aí, delegamos preocupações não relacionadas à IU ao ViewModel ou ao Model.
jbe
2
Agora te entendo mais! Boa discussão com você!
Nam G VU
73

Consigo fazer isso funcionar com o .NET 4.5. Parece simples e nenhum terceiro ou código é necessário.

<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
fonte
2
Não parece funcionar para toda a área, por exemplo, faço isso em um painel de encaixe e só funciona onde há algo dentro do painel de encaixe (por exemplo, bloco de texto, imagem), mas não no espaço em branco.
Stephen Drew
3
OK - este castanho antigo de novo ... precisa definir o plano de fundo como transparente para receber eventos do mouse, conforme stackoverflow.com/questions/7991314/…
Stephen Drew
6
Eu estava coçando minha cabeça tentando descobrir por que estava funcionando para todos vocês e não para mim. De repente, percebi que, no contexto do modelo de item, o contexto de dados é o item atual da fonte de itens e não o modelo de visualização da janela principal. Portanto, usei o seguinte para fazê-lo funcionar <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> No meu caso, EditBandCommand é o comando no modelo de visualização da página não na entidade vinculada.
naskew
naskew tinha o molho secreto que eu precisava com MVVM Light, obtendo um parâmetro de comando sendo o objeto de modelo no item de caixa de listagem clicado duas vezes, e o contexto de dados da janela é definido para o modelo de visualização que expõe o comando: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5 de
Só quero adicionar que InputBindingsestão disponíveis no .NET 3.0 e não estão disponíveis no Silverlight.
Martin
44

Eu gosto de usar comportamentos e comandos de comando anexados . Marlon Grech tem uma implementação muito boa do Attached Command Behaviors. Usando isso, poderíamos atribuir um estilo à propriedade ItemContainerStyle do ListView que definirá o comando para cada ListViewItem.

Aqui definimos o comando a ser disparado no evento MouseDoubleClick, e o CommandParameter, será o objeto de dados no qual clicaremos. Aqui estou subindo na árvore visual para obter o comando que estou usando, mas você poderia facilmente criar comandos para todo o aplicativo.

<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 os comandos, você pode implementar um ICommand diretamente ou usar alguns dos ajudantes como aqueles que vêm no MVVM Toolkit .

rmoore
fonte
1
+1 Descobri que esta é minha solução preferida ao trabalhar com a Orientação de aplicativos compostos para WPF (Prisma).
Travis Heseman
1
O que o namespace 'acb:' representa em sua amostra de código acima?
Nam G VU
@NamGiVU acb:= AttachedCommandBehavior. O código pode ser encontrado no primeiro link da resposta
Rachel
Eu tentei exatamente isso e obtive exceção de ponteiro nulo da classe CommandBehaviorBinding linha 99. a variável "estratégia" é nula. o que há de errado?
etwas77 de
13

Eu descobri uma maneira muito fácil e limpa de fazer isso com os gatilhos de eventos do Blend SDK. MVVM limpo, reutilizável e sem code-behind.

Você provavelmente já tem algo assim:

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

Agora inclua um ControlTemplate para ListViewItem como este se você ainda não usa um:

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

O GridViewRowPresenter será a raiz visual de todos os elementos "internos" que constituem um elemento de linha da lista. Agora poderíamos inserir um gatilho lá para procurar eventos roteados MouseDoubleClick e chamar um comando por meio 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>

Se você tiver elementos visuais "acima" do GridRowPresenter (provavelmente começando com uma grade), você também pode colocar o Trigger lá.

Infelizmente, os eventos MouseDoubleClick não são gerados a partir de todos os elementos visuais (eles são de Controls, mas não de FrameworkElements, por exemplo). Uma solução alternativa é derivar uma classe de EventTrigger e procurar MouseButtonEventArgs com ClickCount de 2. Isso filtra efetivamente todos os não-MouseButtonEvents e todos os MoseButtonEvents com 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);
        }
    }
}

Agora podemos escrever isso ('h' é o namespace da classe auxiliar acima):

<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
fonte
Como descobri, se você colocar o Trigger diretamente no GridViewRowPresenter, pode haver um problema. Os espaços vazios entre as colunas provavelmente não recebem eventos do mouse (provavelmente uma solução alternativa seria estilizá-los com alongamento de alinhamento).
Gunter
Nesse caso, provavelmente é melhor colocar uma grade vazia ao redor do GridViewRowPresenter e colocar o gatilho lá. Isso parece funcionar.
Gunter
1
Observe que você perderá o estilo padrão do ListViewItem se substituir o modelo desta forma. Não importava para o aplicativo no qual eu estava trabalhando, pois ele estava usando um estilo altamente personalizado de qualquer maneira.
Gunter
6

Sei que essa discussão já existe há um ano, mas com o .NET 4, há alguma ideia sobre essa solução? Eu concordo totalmente que o objetivo do MVVM NÃO é eliminar um arquivo code-behind. Também sinto fortemente que só porque algo é complicado, não significa que seja melhor. Aqui está o que coloquei no código por trás:

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }
Aaron
fonte
12
Você deve visualizar o modelo deve ter nomes que representam as ações que você pode executar em seu domínio. O que é uma ação "ButtonClick" em seu domínio? ViewModel representa a lógica do domínio em um contexto amigável à visualização, não é apenas um auxiliar para a visualização. Portanto: ButtonClick nunca deve estar no modelo de exibição, use viewModel.DeleteSelectedCustomer ou o que quer que esta ação realmente represente.
Marius
4

Você pode usar o recurso Action do Caliburn para mapear eventos para métodos em seu ViewModel. Supondo que você tenha um ItemActivatedmétodo em seu ViewModel, o XAML correspondente seria semelhante a:

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

Para obter mais detalhes, você pode examinar a documentação e as amostras do Caliburn.

idursun
fonte
4

Estou achando mais simples vincular o comando quando a visualização é criada:

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

No meu caso se BindAndShowparece com isto (updatecontrols + avalondock):

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

Embora a abordagem deva funcionar com qualquer método que você tenha para abrir novas visualizações.

Timothy Pratley
fonte
Parece-me que esta é a solução mais simples, em vez de tentar fazê-la funcionar apenas em XAML.
Mas
1

Eu vi a solução do rushui com o InuptBindings, mas ainda não consegui acertar a área do ListViewItem onde não havia texto - mesmo depois de definir o fundo como transparente, então resolvi usando diferentes modelos.

Este modelo é para quando o ListViewItem foi selecionado e está ativo:

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

Este modelo é para quando o ListViewItem foi selecionado e está inativo:

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

Este é o estilo padrão usado 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>

O que eu não gosto é a repetição do TextBlock e sua vinculação de texto, não sei se posso declarar isso em apenas um local.

Espero que isso ajude alguém!

user3235445
fonte
Esta é uma ótima solução e eu uso uma semelhante, mas você realmente só precisa de um modelo de controle. Se um usuário vai clicar duas vezes em a listviewitem, provavelmente não se importará se já está selecionado ou não. Também é importante observar que o efeito de realce também pode precisar ser ajustado para combinar com o listviewestilo. Votado.
David Bentley
1

Consigo fazer essa funcionalidade com a estrutura .Net 4.7 usando a biblioteca de interatividade, antes de mais nada, certifique-se de declarar o namespace no arquivo XAML

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

Em seguida, defina o Event Trigger com seu respectivo InvokeCommandAction dentro do ListView como abaixo.

Visão:

<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 o código acima deve ser o suficiente para fazer o evento de clique duplo funcionar em seu ViewModel, no entanto, adicionei a você a classe Model e a classe View Model do meu exemplo para que você tenha uma ideia 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
            }
        }
}

Caso você precise da implementação da classe DelegateCommand .

luis_laurent
fonte
0

Aqui está um comportamento que faz isso em ambos ListBoxe 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
}

Aqui está a classe de extensão usada para localizar o pai.

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
fonte