Запуск події подвійного клацання з елемента WPF ListView за допомогою MVVM


102

У програмі WPF, що використовує MVVM, у мене є контролер користувача з елементом перегляду списку. Під час роботи воно використовуватиме прив'язку даних для заповнення списку перегляду колекцією об'єктів.

Який правильний спосіб приєднати подію подвійного клацання до елементів списку перегляду, щоб, коли елемент у поданні списку подвійний клацання, відповідна подія в моделі перегляду запускається та має посилання на елемент, який натиснув?

Як це можна зробити чистим способом MVVM, тобто відсутність коду позаду у Перегляді?

Відповіді:


76

Будь ласка, код поза не зовсім поганий. На жаль, досить багато людей у ​​спільноті WPF помилилися.

MVVM не є схемою для усунення коду позаду. Потрібно відокремити частину перегляду (зовнішній вигляд, анімацію тощо) від логічної частини (робочого процесу). Крім того, ви можете перевірити логічну частину.

Я знаю достатньо сценаріїв, коли потрібно писати код позаду, тому що прив'язка даних - це не рішення всього. У вашому сценарії я б обробляв подію DoubleClick в коді за файлом і делегував цей виклик ViewModel.

Зразкові програми, які використовують код позаду та все ще виконують розділення MVVM, можна знайти тут:

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


5
Добре сказано, я відмовляюсь використовувати весь цей код та додаткову DLL лише для подвійного клацання!
Едуардо Мольтені

4
Це єдине використання прив'язки - це справжній головний біль. Це як би просити кодувати 1 руку, 1 око на пластир для очей та стоячи на 1 нозі. Подвійне клацання повинно бути простим, і я не бачу, як вартує весь цей додатковий код.
Ехібан

1
Боюся, я не з тобою повністю згоден. Якщо ви говорите «Код позаду - це не погано», то у мене виникає питання з цього приводу: чому ми не делегуємо подію натискання кнопки, а часто використовуємо прив’язку (використовуючи властивість Command)?
Нам Г ВУ

21
@ Nam Gi VU: Я завжди віддаю перевагу прив'язці команд, коли це підтримується керуванням WPF. Прив'язка команд більше, ніж просто передача події "Клік" ViewModel (наприклад, CanExecute). Але команди доступні лише для найпоширеніших сценаріїв. Для інших сценаріїв ми можемо використовувати файл із кодом, і делегуємо проблеми, що не стосуються інтерфейсу користувача, ViewModel або Model.
jbe

2
Тепер я вас більше розумію! Приємної дискусії з вами!
Нам Г ВУ

73

Я можу змусити це працювати з .NET 4.5. Здається, прямо вперед і поза сторонніми сторонами чи кодом не потрібно.

<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
Здається, це не працює для всієї області, наприклад, я роблю це на док-панелі, і вона працює лише там, де є щось на панелі док-станції (наприклад, текстовий блок, зображення), але не порожнє місце.
Стівен Дрю

3
Гаразд - цей старий каштан знову ... потрібно встановити фон прозорим для прийому подій миші, відповідно до stackoverflow.com/questions/7991314/…
Стівен Дрю

6
Я чухав голову, намагаючись зрозуміти, чому це працює для всіх вас, а не для мене. Я раптом зрозумів, що в контексті шаблону елемента контекст даних є поточним елементом з itemssource, а не моделлю перегляду головного вікна. Тому я використав наступне, щоб змусити його працювати <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Тип вікна}}}" /> У моєму випадку EditBandCommand є команда у модулі перегляду сторінки не на пов'язаному об'єкті.
nakew

Naskew мав потрібний мені секретний соус із MVVM Light, отримуючи командний параметр, що є об'єктом моделі у списку, що двічі клацнув, і контекст даних цього вікна встановлюється в модель перегляду, яка виставляє команду: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Тип вікна}}} "CommandParameter =" {Прив'язування} "/>
MC5

Просто хочу додати , що InputBindingsдоступні з .NET 3.0 і НЕ доступні в Silverlight.
Мартін

44

Мені подобається використовувати поведінку та команди вкладених команд. Марлон Грех дуже добре реалізує поведінку приєднаного командування. Використовуючи ці, ми зможемо призначити стиль властивості ListContainerStyle ListView, який встановить команду для кожного ListViewItem.

Тут ми встановлюємо команду, яку потрібно запустити на подію MouseDoubleClick, і CommandParameter буде об’єктом даних, на який ми натискаємо. Ось я подорожую по візуальному дереву, щоб отримати команду, яку я використовую, але ви можете так само легко створити команди для широкого застосування.

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

Для команд можна або безпосередньо реалізувати ICommand , або використовувати деякі помічники, наприклад, ті, що входять до інструментарію MVVM .


1
+1 Я вважав, що це є моїм кращим рішенням під час роботи з Композиційним керівництвом програми для WPF (призми).
Тревіс Гесэман

1
Що означає простір імен "acb:" у вашому коді зразка вище?
Нам Г ВУ

@NamGiVU acb:= AttachedCommandBehavior. Код можна знайти за першим посиланням у відповіді
Рейчел

Я спробував саме це і отримав виключення нульового вказівника з класу CommandBehaviorBinding рядок 99. змінна "стратегія" - це нуль що не так?
etwas77

13

Я знайшов дуже простий і чистий спосіб зробити це за допомогою тригерів події Blend SDK. Чистий MVVM, багаторазовий і без коду.

Напевно у вас вже є щось подібне:

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

Тепер включіть ControlTemplate для ListViewItem таким чином, якщо ви його ще не використовуєте:

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

GridViewRowPresenter буде візуальним коренем усіх елементів "всередині", складаючи елемент рядка списку. Тепер ми можемо вставити тригер там, щоб шукати маршрутизовані події MouseDoubleClick і викликати команду через InvokeCommandAction так:

<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 (probalby, що починається з сітки), ви також можете поставити туди тригер.

На жаль, події MouseDoubleClick генеруються не з усіх візуальних елементів (наприклад, з елементів керування, але не з FrameworkElements). Вирішення завдання полягає в тому, щоб отримати клас з EventTrigger і шукати MouseButtonEventArgs з ClickCount 2. Це ефективно фільтрує всі не-MouseButtonEvents і всі MoseButtonEvents з 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);
        }
    }
}

Тепер ми можемо записати це ("h" - це область простору імен помічників):

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

Як я з'ясував, якщо поставити тригер безпосередньо на GridViewRowPresenter, може виникнути проблема. Порожні проміжки між стовпчиками, ймовірно, взагалі не отримують подій миші (можливо, вирішення полягатиме в тому, щоб вирішити їх стиль з витягненням вирівнювання).
Gunter

У цьому випадку, мабуть, краще поставити порожню сітку навколо GridViewRowPresenter і поставити туди тригер. Це, здається, працює.
Гюнтер

1
Зауважте, що ви втрачаєте стиль ListViewItem за замовчуванням, якщо замінити такий шаблон. Це не мало значення для програми, над якою я працював, оскільки в будь-якому випадку використовував сильно налаштований стиль.
Gunter

6

Я розумію, що це обговорення вже рік, але з .NET 4, чи є якісь думки щодо цього рішення? Я абсолютно погоджуюся, що суть MVVM НЕ у усуненні коду за файлом. Я також дуже відчуваю, що те, що щось складне, не означає, що це краще. Ось що я вкладаю в код позаду:

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

12
Ви повинні переглядатимодель повинна мати імена, що представляють дії, які ви можете виконати у вашому домені. Що таке дія "ButtonClick" у вашому домені? ViewModel представляє логіку домену в зручному для перегляду контексті, він не є просто помічником у перегляді. Отже: ButtonClick ніколи не повинен знаходитись у моделі перегляду, використовуйте viewModel.DeleteSelectedCustomer або будь-що інше, що це дійсно представляє замість цього.
Маріус

4

Ви можете скористатися функцією "Дія калібру ", щоб зіставити події з методами на ViewModel. Якщо припустити, що у вас є ItemActivatedметод, ваш ViewModelвідповідний XAML виглядатиме так:

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

Для отримання додаткової інформації ви можете ознайомитися з документацією та зразками Калібурна.


4

Мені здається, простіше зв’язати команду, коли створено представлення:

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

У моєму випадку BindAndShowвиглядає приблизно так (updatecontrols + avalondock):

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

Хоча підхід повинен працювати з будь-яким методом відкриття нових поглядів.


Мені здається, що це найпростіше рішення, а не намагатися змусити його працювати тільки в XAML.
Мас

1

Я побачив рішення від рушуї з InuptBindings, але мені все ще не вдалося потрапити на область ListViewItem, де не було тексту - навіть після встановлення фону прозорим, тому я вирішив його, використовуючи різні шаблони.

Цей шаблон призначений, коли вибрано ListViewItem і є активним:

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

Цей шаблон призначений, коли вибрано ListViewItem та неактивне:

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

Це стиль за замовчуванням, який використовується для 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>

Що мені не подобається - це повторення TextBlock та його прив'язка тексту, я не знаю, що я можу обійтись, заявивши про це лише в одному місці.

Я сподіваюся, що це комусь допоможе!


Це чудове рішення, і я використовую подібне, але вам дійсно потрібен лише один шаблон управління. Якщо користувач подвійним клацанням клавіші a listviewitem, їх, мабуть, не хвилює, чи вже вибрано це чи ні. Також важливо відзначити, що ефект підкреслення також може бути підроблений, щоб відповідати listviewстилю. Вгору проголосували.
Девід Бентлі

1

Мені вдалося зробити цю функціональність за допомогою .Net 4.7 за допомогою бібліотеки інтерактивності, перш за все переконайтесь у оголошенні простору імен у файлі XAML

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

Потім встановіть тригер події за допомогою відповідної InvokeCommandAction всередині ListView, як показано нижче.

Вид:

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

Адаптація коду вище повинна бути достатньою для того, щоб подія подвійного клацання працювала на вашому ViewModel, однак я додав вам клас Model та View Model зі свого прикладу, щоб ви мали повну ідею.

Модель:

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

    public string DevelopedBy { get; set; }
}

Переглянути модель:

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 .


0

Ось така поведінка, яка це робиться як на, так ListBoxі на 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
}

Ось клас розширення, який використовується для пошуку батьків.

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

Використання:

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>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.