Kích hoạt sự kiện nhấp đúp từ một mục WPF ListView bằng MVVM


102

Trong ứng dụng WPF sử dụng MVVM, tôi có quyền điều khiển người dùng với mục listview. Trong thời gian chạy, nó sẽ sử dụng databinding để lấp đầy listview với một bộ sưu tập các đối tượng.

Cách chính xác để đính kèm sự kiện nhấp đúp vào các mục trong chế độ xem danh sách để khi một mục trong chế độ xem danh sách được nhấp đúp, Sự kiện tương ứng trong mô hình chế độ xem được kích hoạt và có tham chiếu đến mục được nhấp?

Làm thế nào nó có thể được thực hiện theo cách MVVM sạch, tức là không có mã phía sau trong Chế độ xem?

Câu trả lời:


76

Xin vui lòng, mã phía sau không phải là một điều xấu. Thật không may, khá nhiều người trong cộng đồng WPF đã hiểu sai điều này.

MVVM không phải là một mẫu để loại bỏ mã đằng sau. Nó là để tách phần xem (giao diện, hình ảnh động, v.v.) khỏi phần logic (quy trình làm việc). Hơn nữa, bạn có thể kiểm tra đơn vị phần logic.

Tôi biết đủ các tình huống mà bạn phải viết mã vì ràng buộc dữ liệu không phải là giải pháp cho mọi thứ. Trong trường hợp của bạn, tôi sẽ xử lý sự kiện DoubleClick trong mã phía sau tệp và ủy quyền lệnh gọi này cho ViewModel.

Bạn có thể tìm thấy các ứng dụng mẫu sử dụng mã phía sau và vẫn đáp ứng phân tách MVVM tại đây:

Khung ứng dụng WPF (WAF) - http://waf.codeplex.com


5
Nói tốt, tôi từ chối sử dụng tất cả mã đó và một DLL bổ sung chỉ để thực hiện một cú nhấp đúp!
Eduardo Molteni

4
Điều này chỉ sử dụng Ràng buộc là làm cho tôi thực sự đau đầu. Nó giống như được yêu cầu viết mã bằng 1 tay, 1 mắt trên miếng che mắt và đứng trên 1 chân. Nhấp đúp nên đơn giản và tôi không thấy tất cả mã bổ sung này có giá trị như thế nào.
Echiban

1
Tôi e rằng tôi không hoàn toàn đồng ý với bạn. Nếu bạn nói 'mã phía sau không tệ', thì tôi có một câu hỏi về điều đó: Tại sao chúng ta không ủy quyền sự kiện nhấp chuột cho nút mà thường sử dụng liên kết (sử dụng thuộc tính Lệnh) để thay thế?
Nam G VU

21
@Nam Gi VU: Tôi luôn thích Command Binding hơn khi nó được hỗ trợ bởi WPF Control. Command Binding không chỉ chuyển tiếp sự kiện 'Click' tới ViewModel (ví dụ: CanExecute). Nhưng Lệnh chỉ có sẵn cho các tình huống phổ biến nhất. Đối với các tình huống khác, chúng tôi có thể sử dụng tệp mã phía sau và ở đó, chúng tôi ủy thác các mối quan tâm liên quan đến giao diện người dùng cho ViewModel hoặc Model.
jbe

2
Bây giờ tôi hiểu bạn hơn! Rất vui được thảo luận với bạn!
Nam G VU

73

Tôi có thể làm cho điều này hoạt động với .NET 4.5. Có vẻ như thẳng về phía trước và không cần bên thứ ba hoặc mã phía sau.

<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
Dường như không hoạt động cho toàn bộ khu vực, ví dụ: tôi thực hiện việc này trên bảng điều khiển dock và nó chỉ hoạt động khi có thứ gì đó bên trong bảng điều khiển dock (ví dụ: khối văn bản, hình ảnh) chứ không phải khoảng trống.
Stephen Drew

3
OK - hạt dẻ cũ này một lần nữa ... cần phải thiết lập nền để minh bạch để nhận các sự kiện chuột, theo stackoverflow.com/questions/7991314/...
Stephen Drew

6
Tôi đang vò đầu bứt tai để tìm hiểu xem tại sao nó lại hiệu quả với tất cả các bạn mà không phải cho tôi. Tôi đột nhiên nhận ra rằng trong ngữ cảnh của mẫu mục, ngữ cảnh dữ liệu là mục hiện tại từ nguồn mục chứ không phải mô hình chế độ xem của cửa sổ chính. Vì vậy, tôi đã sử dụng cách sau để làm cho nó hoạt động <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> Trong trường hợp của tôi, EditBandCommand là lệnh trên mô hình xem của trang không có trên thực thể bị ràng buộc.
naskew

naskew có nước sốt bí mật mà tôi cần với MVVM Light, nhận tham số lệnh là đối tượng mô hình trong listboxitem được nhấp đúp và ngữ cảnh dữ liệu của cửa sổ được đặt thành mô hình xem hiển thị lệnh: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5

Chỉ muốn thêm InputBindingsnhững thứ có sẵn từ .NET 3.0 và không có sẵn trong Silverlight.
Martin

44

Tôi thích sử dụng Hành vi Lệnh và Lệnh đính kèm . Marlon Grech đã thực hiện rất tốt các Hành vi Chỉ huy Đính kèm. Sử dụng chúng, sau đó chúng ta có thể gán một kiểu cho thuộc tính ItemContainerStyle của ListView sẽ đặt lệnh cho mỗi ListViewItem.

Ở đây chúng tôi đặt lệnh sẽ được kích hoạt trên sự kiện MouseDoubleClick và CommandParameter, sẽ là đối tượng dữ liệu mà chúng tôi nhấp vào. Ở đây, tôi đang đi lên cây trực quan để lấy lệnh mà tôi đang sử dụng, nhưng bạn có thể dễ dàng tạo các lệnh trên toàn ứng dụng.

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

Đối với các lệnh, bạn có thể triển khai ICommand trực tiếp hoặc sử dụng một số trình trợ giúp như các trình trợ giúp có trong Bộ công cụ MVVM .


1
+1 Tôi thấy đây là giải pháp ưa thích của tôi khi làm việc với Hướng dẫn ứng dụng tổng hợp cho WPF (Prism).
Travis Heseman

1
Không gian tên 'acb:' đại diện cho điều gì trong đoạn mã mẫu của bạn?
Nam G VU

@NamGiVU acb:= AttachedCommandBehavior. Mã này có thể được tìm thấy trong các liên kết đầu tiên trong câu trả lời
Rachel

tôi đã thử chỉ điều đó và nhận được ngoại lệ con trỏ null từ dòng CommandBehaviorBinding lớp 99. biến "chiến lược" là null. chuyện gì vậy?
etwas77

13

Tôi đã tìm thấy một cách rất dễ dàng và rõ ràng để thực hiện việc này với các trình kích hoạt Sự kiện Blend SDK. MVVM sạch, có thể tái sử dụng và không có mã phía sau.

Bạn có thể đã có một cái gì đó như thế này:

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

Bây giờ, hãy bao gồm ControlTemplate cho ListViewItem như thế này nếu bạn chưa sử dụng:

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

GridViewRowPresenter sẽ là gốc trực quan của tất cả các phần tử "bên trong" tạo thành phần tử hàng danh sách. Bây giờ chúng ta có thể chèn một trình kích hoạt ở đó để tìm kiếm các sự kiện được định tuyến MouseDoubleClick và gọi một lệnh thông qua InvokeCommandAction như sau:

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

Nếu bạn có các yếu tố hình ảnh "phía trên" GridRowPresenter (probalby bắt đầu bằng lưới), bạn cũng có thể đặt Trigger ở đó.

Rất tiếc, các sự kiện MouseDoubleClick không được tạo từ mọi yếu tố hình ảnh (chúng từ Controls, nhưng không phải từ FrameworkElements chẳng hạn). Một cách giải quyết là lấy một lớp từ EventTrigger và tìm MouseButtonEventArgs với ClickCount là 2. Điều này lọc ra một cách hiệu quả tất cả những không phải MouseButtonEvents và tất cả MoseButtonEvents có 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);
        }
    }
}

Bây giờ chúng ta có thể viết thế này ('h' là Không gian tên của lớp trình trợ giúp ở trên):

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

Khi tôi phát hiện ra nếu bạn đặt Trigger trực tiếp trên GridViewRowPresenter, có thể có vấn đề. Các khoảng trống giữa các cột có thể hoàn toàn không nhận được các sự kiện chuột (có thể một cách giải quyết là tạo kiểu cho chúng với độ giãn của căn chỉnh).
Gunter

Trong trường hợp này, có lẽ tốt hơn là đặt một lưới trống xung quanh GridViewRowPresenter và đặt trình kích hoạt ở đó. Điều này có vẻ hiệu quả.
Gunter

1
Lưu ý rằng bạn sẽ mất kiểu mặc định cho ListViewItem nếu bạn thay thế mẫu như thế này. Nó không quan trọng đối với ứng dụng tôi đang làm việc vì dù sao thì nó cũng đang sử dụng một kiểu dáng được tùy chỉnh nhiều.
Gunter

6

Tôi nhận ra rằng cuộc thảo luận này đã được một năm tuổi, nhưng với .NET 4, có bất kỳ suy nghĩ nào về giải pháp này không? Tôi hoàn toàn đồng ý rằng quan điểm của MVVM là KHÔNG loại bỏ mã đằng sau tệp. Tôi cũng cảm thấy rất rõ ràng rằng chỉ vì một cái gì đó phức tạp, không có nghĩa là nó tốt hơn. Đây là những gì tôi đặt trong mã đằng sau:

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

12
Bạn nên xem mô hình phải có tên đại diện cho các hành động bạn có thể thực hiện trong miền của mình. Hành động "ButtonClick" trong miền của bạn là gì? ViewModel đại diện cho logic của miền trong bối cảnh thân thiện với chế độ xem, nó không chỉ là trình trợ giúp cho chế độ xem. Vì vậy: Không bao giờ nên sử dụng ButtonClick trên viewmodel, hãy sử dụng viewModel.DeleteSelectedCustomer hoặc bất kỳ hành động nào thực sự đại diện cho hành động này.
Marius

4

Bạn có thể sử dụng tính năng Hành động của Caliburn để ánh xạ các sự kiện tới các phương pháp trên ViewModel của bạn. Giả sử bạn có một ItemActivatedphương thức trên của mình ViewModel, thì XAML tương ứng sẽ giống như sau:

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

Để biết thêm chi tiết, bạn có thể kiểm tra tài liệu và mẫu của Caliburn.


4

Tôi thấy việc liên kết lệnh khi chế độ xem được tạo đơn giản hơn:

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

Trong trường hợp của tôi BindAndShowtrông như thế này (updatecontrols + avalondock):

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

Mặc dù cách tiếp cận này sẽ hoạt động với bất kỳ phương pháp nào bạn có để mở ra các khung nhìn mới.


Đối với tôi, có vẻ như đây là giải pháp đơn giản nhất, thay vì cố gắng làm cho nó chỉ hoạt động trong XAML.
Mas

1

Tôi đã thấy giải pháp từ rushui với InuptBindings nhưng tôi vẫn không thể truy cập vào khu vực ListViewItem nơi không có văn bản - ngay cả sau khi đặt nền thành trong suốt, vì vậy tôi đã giải quyết bằng cách sử dụng các mẫu khác nhau.

Mẫu này dành cho khi ListViewItem đã được chọn và đang hoạt động:

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

Mẫu này dành cho khi ListViewItem đã được chọn và không hoạt động:

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

Đây là kiểu mặc định được sử dụng cho 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>

Điều tôi không thích là sự lặp lại của TextBlock và liên kết văn bản của nó, tôi không biết II có thể khai báo điều đó chỉ ở một vị trí.

Tôi hi vọng điêu nay se giup được ai đo!


Đây là một giải pháp tuyệt vời và tôi sử dụng một giải pháp tương tự, nhưng bạn thực sự chỉ cần một mẫu điều khiển. Nếu người dùng định nhấp đúp vào a listviewitem, họ có thể không quan tâm xem nó đã được chọn hay chưa. Ngoài ra, điều quan trọng cần lưu ý là hiệu ứng đánh dấu cũng có thể cần được tinh chỉnh để phù hợp với listviewphong cách. Đã bình chọn.
David Bentley

1

Tôi đã thành công khi thực hiện chức năng này với .Net 4.7 framework bằng cách sử dụng thư viện tương tác, trước hết hãy đảm bảo khai báo không gian tên trong tệp XAML

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

Sau đó đặt Event Trigger với InvokeCommandAction tương ứng của anh ấy bên trong ListView như bên dưới.

Lượt xem:

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

Việc điều chỉnh mã ở trên là đủ để làm cho sự kiện nhấp đúp hoạt động trên ViewModel của bạn, tuy nhiên, tôi đã thêm cho bạn lớp Model và View Model từ ví dụ của tôi để bạn có thể có đầy đủ ý tưởng.

Mô hình:

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

    public string DevelopedBy { get; set; }
}

Xem mô hình:

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

Trong trường hợp bạn cần triển khai lớp DelegateCommand .


0

Đây là một hành vi được thực hiện trên cả ListBoxListView.

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
}

Đây là lớp mở rộng được sử dụng để tìm cha mẹ.

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

Sử dụng:

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>
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.