Как в ListView можно обработать двойной клик ListViewItem'а

Описание начну с количества лирики. Многие только увидев WPF говорят что он очень беден в отношении функционала стандартных контролов, если честно то по началу и я так думал (хотя может быть только я один так думал :). Такие мысли возникали когда приделывал ListView сортировки, выравнивание строк в ячейках и несколько других функций которыми не обладал стандартный ListView, но проделав все это понимаешь что на самом деле это не есть недостаток, а даже совсем наоборот. Т.е. я хочу сказать что весь функционал в итоге можно сделать так как ты себе его представляешь, а не пытаться переделывать то что кто то сделал ранее, как это было в WindowsForms и раньше. Ну да ладно, приступим к самой сути которую я собираюсь описать в этой статье.

Оговорюсь сразу, все подходы будут рассматриваться относительно модификации паттерна MVC которую вы могли видеть в предыдущей моей статье и которая успешно применяется в проекте разрабатываемом на фирме в которой я тружусь. Так вот, до недавнего времени для обработки двойного клика у ListViewItem применялась следующая нехитрая конструкция:

простой пример XAML разметки для списка

       <ListView x:Name='lv' Grid.Row="2" ItemsSource="{Binding Data}" SelectedValue="{Binding SelectedItem}" MouseDoubleClick="ListView_MouseDoubleClick">
           <ListView.View>
               <GridView AllowsColumnReorder="true">
                   <GridViewColumn DisplayMemberBinding="{Binding Path=Id}" Width="50" Header="Код"/>
                   //.....................................
                   <GridViewColumn DisplayMemberBinding="{Binding Path=Notes}" Width="150" Header="Примечание"/>
               </GridView>
           </ListView.View>
       </ListView>
 

ну и естественно обработчик двойного клика MouseDoubleClick=«ListView_MouseDoubleClick». Но что здесь не так, а то что данный клик обрабатывается для всего ListView независимо от того где был он сделан. Дополнительная проблема открывается если мы имеем два списка, то есть один в ListViewItem.Template другого, суть проблемы вы увидите в том какой код нужен для того чтобы вычленить именно клик ListViewItem.

        private void ListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            var dep = (DependencyObject)e.OriginalSource;
            while (dep != null)
            {
                dep = VisualTreeHelper.GetParent(dep);
                if (dep is ListViewItem)
                {
                    Presenter.EditDoc();
                    return;
                }
            }
        }

Как видите нам приходится идти по всему визуальному дереву пока мы не определим нужный элемент. Для тех кто не понимает почему нельзя просто привязать событие к ListViewItem сделаю набольшое отступление. Рассмотрим следующий код:

       <ListView x:Name='lv' Grid.Row="2" ItemsSource="{Binding Data}" SelectedValue="{Binding SelectedItem}">
           <ListViewItem MouseDoubleClick="ListView_MouseDoubleClick"/>
           <ListView.View>
               <GridView AllowsColumnReorder="true">
                   <GridViewColumn DisplayMemberBinding="{Binding Path=Id}" Width="50" Header="Код"/>
                   //.....................................
                   <GridViewColumn DisplayMemberBinding="{Binding Path=Notes}" Width="150" Header="Примечание"/>
               </GridView>
           </ListView.View>
       </ListView>

В данном случае мы получим ошибку гласящую примерно о следующем, «чтобы назначить ItemsSource список ListViewItems должен быть пуст», ну или что то вроде этого. Как же обойти данную проблему. Так вот, немного погуглив я наткнулся на следующее решение на сайте Microsoft:

<Style TargetType="ListViewItem">
           <EventSetter Event="MouseDoubleClick" Handler="ListViewItem_MouseDoubleClick"/>
</Style>

Наткнувшись на него я было обрадовался простоте решения, но немного подумал и пришёл к выводу что данный вариант почти ничем не отличается от моего первого решения. По сути он так же перенаправляет ListView_MouseDoubleClick на ListViewItem_MouseDoubleClick. И второе самое главное что не нравилось мне в обоих случаях это невозможность использовать команды. Продолжив поиски решения на просторах гугла я не нашёл больше ни одного более или менее интересного и тем более работающего примера. Но не опускать же руки. Так вот в очередной раз пришлось как говориться пораскинуть мозгами и вот к чему я пришёл. Первое что пришло в голову это то что почему бы не научить ListViewItem принимать вызовы команд самому не ожидая тычка от родителя. Нет ничего проще скажете вы, отнаследуемся от ListViewItem и вперед, но данный подход меня не устраивает, особенно учитывая то что в последнее время мне попалось несколько статей в которых наследование не особо поощрялось и говорилось о том что оно таит в себе больше зла чем добра, углубляться в эту тему не буду, а просто отмажусь фразой что мода на наследование прошла. Но какая альтернатива, а вот какая, основная мощь WPF это всякого рода темплейты, почему бы не применить их. И вот что у меня получилось.

           <Grid>
               <Grid.Resources>
                   <ControlTemplate x:Key="xTmpl" TargetType='{x:Type ListViewItem}'>
                       <StackPanel>
                           <StackPanel.InputBindings>
                               <MouseBinding Command="ApplicationCommands.Open" MouseAction="LeftDoubleClick"/>
                           </StackPanel.InputBindings>
                           <GridViewRowPresenter Content="{TemplateBinding Content}" 
                                         Columns="{TemplateBinding GridView.ColumnCollection}"/>
                       </StackPanel>
                   </ControlTemplate>
                   <ControlTemplate x:Key="Selected" TargetType='{x:Type ListViewItem}'>
                       <StackPanel Background="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}">
                           <StackPanel.InputBindings>
                               <MouseBinding Command="ApplicationCommands.Open" MouseAction="LeftDoubleClick"/>
                           </StackPanel.InputBindings>
                           <GridViewRowPresenter Content="{TemplateBinding Content}" 
                                         Columns="{TemplateBinding GridView.ColumnCollection}" TextBlock.Foreground="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                       </StackPanel>
                   </ControlTemplate>
               </Grid.Resources>
               <ListView ItemsSource="{Binding }" SelectedValue="{Binding }" >
                   <ListView.ItemContainerStyle>
                       <Style TargetType="{x:Type ListViewItem}"  >
                           <Setter Property="Template" Value="{StaticResource xTmpl}"/>
                           <Style.Triggers>
                               <Trigger Property="IsSelected" Value="True">
                                   <Setter Property="Template" Value="{StaticResource Selected}"/>
                               </Trigger>
                           </Style.Triggers>
                       </Style>
                   </ListView.ItemContainerStyle>
                   <ListView.View>
                       <GridView AllowsColumnReorder="True">
                           //................................
                       </GridView>
                   </ListView.View>
               </ListView>
           </Grid>
 

Думаю суть предельна ясна, в ControlTemplate итема кладём контейнер который принимает на себя клики и их же мы ловим и обрабатываем. Пару слов о том, зачем я сделал два ControlTemplate, ответ прост, один для ListViewItem в обычном состоянии, а второй для выбранного ListViewItem. Вот и все, надеюсь что эта статья помогла вам и показала что в WPF главной загвоздкой является отсутствие фантазии…