WPF/MVVM - как обрабатывать двойной щелчок на TreeViewItems в ViewModel?
(Примечание - это повторная публикация, поскольку мой первый вопрос был опубликован под неправильным заголовком: здесь Извините!)
У меня есть стандартный вид дерева WPF и связанные элементы для просмотра классов моделей.
Теперь я хочу обработать поведение при двойном щелчке элементов (открытие документов в визуальном стиле студии).
Я могу получить обработчик событий для запуска в элементе управления, содержащем древовидное представление (показан xaml), но как мне привязать к определенному поведению на классах модели представления - например, ProjectViewModel?
Предпочтительно привязывать к ICommand-Implementer, так как это используется в других местах...
<TreeView ItemsSource="{Binding Projects}" MouseDoubleClick="TreeView_MouseDoubleClick">
<TreeView.ItemContainerStyle>
<!--
This Style binds a TreeViewItem to a TreeViewItemViewModel.
-->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type Implementations:ProjectViewModel}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\Region.png" />
<TextBlock Text="{Binding DisplayName}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type Implementations:PumpViewModel}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\State.png" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type Implementations:PumpDesignViewModel}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\City.png" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
9 ответов
Обновление моего ответа немного.
Я пробовал много разных подходов для этого, и я все еще чувствую, что Attached Behaviors - лучшее решение. Хотя вначале это может показаться много накладных расходов, на самом деле это не так. Я сохраняю все свое поведение для ICommands
в том же месте, и когда мне нужна поддержка для другого события, это просто вопрос копирования / вставки и изменения события в PropertyChangedCallback
,
Я также добавил дополнительную поддержку для CommandParameter
,
В дизайнере это просто вопрос выбора нужного события
Вы можете установить это либо на TreeView
, TreeViewItem
или любое другое место, которое вам нравится.
Пример. Установите его на TreeView
<TreeView commandBehaviors:MouseDoubleClick.Command="{Binding YourCommand}"
commandBehaviors:MouseDoubleClick.CommandParameter="{Binding}"
.../>
Пример. Установите его на TreeViewItem
<TreeView ItemsSource="{Binding Projects}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="commandBehaviors:MouseDoubleClick.Command"
Value="{Binding YourCommand}"/>
<Setter Property="commandBehaviors:MouseDoubleClick.CommandParameter"
Value="{Binding}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
А вот и прикрепленное поведение MouseDoubleClick
public class MouseDoubleClick
{
public static DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(MouseDoubleClick),
new UIPropertyMetadata(CommandChanged));
public static DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter",
typeof(object),
typeof(MouseDoubleClick),
new UIPropertyMetadata(null));
public static void SetCommand(DependencyObject target, ICommand value)
{
target.SetValue(CommandProperty, value);
}
public static void SetCommandParameter(DependencyObject target, object value)
{
target.SetValue(CommandParameterProperty, value);
}
public static object GetCommandParameter(DependencyObject target)
{
return target.GetValue(CommandParameterProperty);
}
private static void CommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
Control control = target as Control;
if (control != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
control.MouseDoubleClick += OnMouseDoubleClick;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
control.MouseDoubleClick -= OnMouseDoubleClick;
}
}
}
private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
ICommand command = (ICommand)control.GetValue(CommandProperty);
object commandParameter = control.GetValue(CommandParameterProperty);
command.Execute(commandParameter);
}
}
Я опоздал на это, но я просто использовал другое решение. Еще раз, это может быть не лучшим, но вот как я это сделал.
Прежде всего, предыдущий ответ от Meleak - это круто, но я чувствую, что очень тяжело быть вынужденным добавлять AttachedBehaviors только для чего-то столь же простого, как MouseDoubleClick. Это заставило бы меня использовать новый шаблон в моем приложении и еще более усложнило бы все.
Моя цель - оставаться максимально простым. Поэтому я сделал кое-что очень простое (мой пример для DataGrid, но вы можете использовать это на множестве различных элементов управления):
<DataGrid MouseDoubleClick="DataGrid_MouseDoubleClick">
<!-- ... -->
</DataGrid>
В коде позади:
private void DataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
//Execute the command related to the doubleclick, in my case Edit
(this.DataContext as VmHome).EditAppCommand.Execute(null);
}
Почему я чувствую, что это не нарушает MVVM-паттерн? Потому что, по моему мнению, единственные вещи, которые вы должны поместить в код позади, - это мосты к вашей viewModel, вещи, очень специфичные для вашего пользовательского интерфейса. В этом случае это просто говорит о том, что если вы дважды щелкнете, запустите соответствующую команду. Это почти то же самое, что Command="{Binding EditAppCommand}", я просто симулировал это поведение.
Не стесняйтесь высказать мне свое мнение по этому поводу, я был бы рад услышать некоторых критиков этого мышления, но на данный момент я считаю, что это самый простой способ реализовать его, не ломая MVVM.
Рекомендации Meleak и igor хороши, но когда обработчик события двойного щелчка привязан к TreeViewItem
затем этот обработчик события вызывается для всех родительских элементов элемента (а не только для элемента, по которому щелкнули). Если это не желательно, вот еще одно дополнение:
private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
ICommand command = (ICommand)control.GetValue(CommandProperty);
object commandParameter = control.GetValue(CommandParameterProperty);
if (sender is TreeViewItem)
{
if (!((TreeViewItem)sender).IsSelected)
return;
}
if (command.CanExecute(commandParameter))
{
command.Execute(commandParameter);
}
}
Это действительно просто, и вот как я обработал двойной щелчок на TreeView:
<Window x:Class="TreeViewWpfApplication.MainWindow"
...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
...>
<TreeView ItemsSource="{Binding Departments}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<ei:CallMethodAction MethodName="SomeMethod" TargetObject="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
</Window>
System.Windows.Interactivity.dll берется из C:\Program Files (x86)\Microsoft SDKs\Expression\Blend.NETFramework\v4.0\Libraries\System.Windows.Interactivity.dll или из NuGet.
На мой взгляд модель:
public class TreeViewModel : INotifyPropertyChanged
{
private List<Department> departments;
public TreeViewModel()
{
Departments = new List<Department>()
{
new Department("Department1"),
new Department("Department2"),
new Department("Department3")
};
}
public List<Department> Departments
{
get
{
return departments;
}
set
{
departments = value;
OnPropertyChanged("Departments");
}
}
public void SomeMethod()
{
MessageBox.Show("*****");
}
}
Meleak решение отличное!, но я добавил проверить
private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
ICommand command = (ICommand)control.GetValue(CommandProperty);
object commandParameter = control.GetValue(CommandParameterProperty);
//Check command can execute!!
if(command.CanExecute(commandParameter ))
command.Execute(commandParameter);
}
Привязка мыши к текстовому блоку
В TreeView. Ресурсы представления:
<HierarchicalDataTemplate
DataType="{x:Type treeview:DiscoveryUrlViewModel}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="../Images/ic_search.png" />
<TextBlock Text="{Binding DisplayText}" >
<TextBlock.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding DoubleClickCopyCommand}"
CommandParameter="{Binding }" />
</TextBlock.InputBindings>
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
В ViewModel этого представления (DiscoveryUrlViewModel.cs):
private RelayCommand _doubleClickCommand;
public ICommand DoubleClickCopyCommand
{
get
{
if (_doubleClickCommand == null)
_doubleClickCommand = new RelayCommand(OnDoubleClick);
return _doubleClickCommand;
}
}
private void OnDoubleClick(object obj)
{
var clickedViewModel = (DiscoveryUrlViewModel)obj;
}
Просто для любопытства: что, если я приму участие Фредерикса, но реализую это непосредственно как поведение?
public class MouseDoubleClickBehavior : Behavior<Control>
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof (ICommand), typeof (MouseDoubleClickBehavior), new PropertyMetadata(default(ICommand)));
public ICommand Command
{
get { return (ICommand) GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof (object), typeof (MouseDoubleClickBehavior), new PropertyMetadata(default(object)));
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.MouseDoubleClick += OnMouseDoubleClick;
}
protected override void OnDetaching()
{
AssociatedObject.MouseDoubleClick -= OnMouseDoubleClick;
base.OnDetaching();
}
void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
if (Command == null) return;
Command.Execute(/*commandParameter*/null);
}
}
Прошло 11 лет. Я только что сделал это в своем дереве на основе решения @Damascus.
В Xaml есть UserControl с TreeView. DataType=FileResultBrief - это то, что я хочу дважды щелкнуть.
<resultTrees:ResultTreeView x:Class="ChiSharedFormsWpf.ResultTrees.ChiTreeView"
MouseDoubleClick="ChiTreeView_OnMouseDoubleClick"
d:DesignHeight="450" d:DesignWidth="3800">
<Grid>
<DockPanel HorizontalAlignment="Stretch">
<TreeView Name="Tree" HorizontalAlignment="Stretch">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type dirTrees:FileResultBrief}"
ItemsSource="{Binding BadSmells}">
<StackPanel Orientation="Horizontal" Background="{Binding Index.SoftColorHex}"
Tag="{Binding FileName}">
<fa5:FontAwesome Icon="Regular_FileCode" Margin="0,2,5,0" />
<TextBlock Text="{Binding Index.Brief}" />
<TextBlock Text="{Binding FileName}" Margin="10 0"/>
</StackPanel>
</HierarchicalDataTemplate>
вот что произошло в коде позади:
private void ChiTreeView_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not ChiTreeView {Tree: TreeView {SelectedItem: FileResultBrief brief}})
return;
FileUtility.OpenFileWithDefaultApp(ScanTask.FullNameOf(brief.FileName));
}
Я начал использовать Wpf и Xaml 2 недели назад. Это было сделано путем установки точки останова на входе метода и наблюдения за «отправителем» все глубже и глубже.
Лучший подход, который я достиг, - это просто связать IsSelected
свойство от TreeViewItem до ViewModel в двустороннем режиме и реализовать логику в установщике свойств. Затем вы можете определить, что делать, если значение равно true или false, потому что это свойство будет меняться всякий раз, когда пользователь щелкает элемент.
class MyVM
{
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected == null)
return;
_isSelected = vale;
if (_isSelected)
{
// Your logic goes here.
}
else
{
// Your other logic goes here.
}
}
}
Это позволяет избежать большого количества кода.
Кроме того, этот метод позволяет вам реализовать поведение "onclick" только в моделях представления, которые действительно нуждаются в этом.