Я получаю эту ошибку/предупреждение:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=DrivesListView'
Когда я нажимаю «Обновить», команда не срабатывает. Я предполагаю, что поскольку это ContextMenu, мне нужно каким-то образом получить доступ к родительскому элементу управления в пути привязки, а затем я могу использовать MouseDownCommand
.
MouseDownCommand
находится в моей вкладке viewmodel, TabItemViewModel
. Мой MainWindowViewModel
содержит список TabItemViewModel
, и это источник элементов TabControl
.
Я попытался настроить событие ContextMenu Opened, чтобы вручную установить DataContext, чтобы посмотреть, исправит ли он как-то DataContext:
private void ContextMenu_Opened(object sender, RoutedEventArgs e)
{
ContextMenu menu = sender as ContextMenu;
ListView listView = menu.PlacementTarget as ListView;
Grid grid = listView.Parent as Grid;
TabControl tabControl = grid.Parent as TabControl;
menu.DataContext = (TabItemViewModel)tabControl.SelectedItem;
}
Проблема в том, что я не могу получить tabControl
из grid
. Выполнение .Parent
просто возвращает null
по какой-то неизвестной причине.
Я также попытался установить Tag
элемента управления, что тоже не сработало:
<ListView Grid.Row="1" Name="DrivesListView" ItemsSource="{Binding Drives}"
Tag="{Binding DataContext,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}">>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Refresh">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDown">
<i:InvokeCommandAction
Command="{Binding Path=PlacementTarget.Tag.MouseDownCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"
CommandParameter="{Binding ElementName=DrivesListView}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</MenuItem>
</ContextMenu>
</ListView.ContextMenu>
<ListView.Style>
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource MahApps.Styles.ListView}">
<Style.Triggers>
<DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="true">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="false">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding Path=MouseDoubleClickCommand}"
CommandParameter="{Binding ElementName=DrivesListView}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListView.View>
<GridView>
<GridViewColumn x:Name="NameHeader" Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn x:Name="TypeHeader" Header="Type" DisplayMemberBinding="{Binding Type}"/>
<GridViewColumn x:Name="TotalSizeHeader" Header="Total Size" DisplayMemberBinding="{Binding TotalSize}"/>
<GridViewColumn x:Name="FreeSpaceHeader" Header="Free Space" DisplayMemberBinding="{Binding FreeSpace}"/>
</GridView>
</ListView.View>
</ListView>
Вот мой XAML
<UserControl x:Class="FileExplorerModuleServer.Views.FileBrowserTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--ItemsSource is bound to the 'Tabs' property on the view-
model, while DisplayMemeberPath tells TabControl
which property on each tab has the tab's name -->
<TabControl Name="SubTabControl" Grid.Row="1"
ItemsSource="{Binding Tabs}"
DisplayMemberPath="Header" SelectedIndex="{Binding SelectedIndex}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding Path=TabChangedCommand}"
CommandParameter="{Binding ElementName=SubTabControl}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" HorizontalAlignment="Left" Width="300" Text="{Binding Path}">
</TextBox>
<mah:ProgressRing Grid.Row="1" Height="1">
<mah:ProgressRing.Style>
<Style TargetType="{x:Type mah:ProgressRing}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsLoading}" Value="true">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding IsLoading}" Value="false">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</mah:ProgressRing.Style>
</mah:ProgressRing>
<ListView Grid.Row="1" Name="DrivesListView" ItemsSource="{Binding Drives}">
<ListView.ContextMenu>
<ContextMenu Opened="ContextMenu_Opened">
<MenuItem Header="Refresh">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDown">
<i:InvokeCommandAction
Command="{Binding Path=MouseDownCommand}"
CommandParameter="{Binding ElementName=DrivesListView}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</MenuItem>
</ContextMenu>
</ListView.ContextMenu>
<ListView.Style>
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource MahApps.Styles.ListView}">
<Style.Triggers>
<DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="true">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="false">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding Path=MouseDoubleClickCommand}"
CommandParameter="{Binding ElementName=DrivesListView}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListView.View>
<GridView>
<GridViewColumn x:Name="NameHeader" Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn x:Name="TypeHeader" Header="Type" DisplayMemberBinding="{Binding Type}"/>
<GridViewColumn x:Name="TotalSizeHeader" Header="Total Size" DisplayMemberBinding="{Binding TotalSize}"/>
<GridViewColumn x:Name="FreeSpaceHeader" Header="Free Space" DisplayMemberBinding="{Binding FreeSpace}"/>
</GridView>
</ListView.View>
</ListView>
Вот ViewModel.cs
public ICommand MouseDownCommand
=> new RelayCommand<object>(e => MouseDown(e));
private void MouseDown(object commandParameter)
{
}
Триггер события SelectionChanged
— это беспорядок, просто используйте привязку: SelectedItem="{Binding SelectedTab, Mode=OneWayToSource}"
; затем в виртуальной машине добавьте свойство SelectedTab
типа объектов в коллекции Tabs
. Затем переместите код TabChangedCommand
в установщик свойства SelectedTab
.
ListView.Style
тоже беспорядок, просто используйте BooleanToVisibilityConverter: Visibility="{Binding DrivesListViewEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"
Я рассмотрю эти два изменения, которые вы предложили. Последнее, однако, требует добавления пользовательского класса, если я не ошибаюсь, и это довольно раздражает, чего я пытался избежать.
Во-первых, событие MouseDown
не работает в случае с MenuItem
. Итак, вам нужно использовать событие PreviewMouseDown
или, возможно, Click
внутри цепочки. Кроме того, вы не можете использовать ElementName для ссылки на элементы за пределами ContextMenu
.
Затем, если предположить, что DataContext
из ListView
неявно связано с TabItemViewModel
, и вы хотите указать, что ListView
как CommandParameter
, его можно изменить следующим образом:
<MenuItem Header="Refresh">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction
Command="{Binding PlacementTarget.DataContext.MouseDownCommand, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
CommandParameter="{Binding PlacementTarget, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</MenuItem>
Зачем вам использовать триггер, когда свойство Command
доступно?
Ты прав. В случае события «Клик» использовать InvokeCommandAction
не обязательно.
Можете ли вы объяснить, как работает ваш CommandParameter? Я использовал смесь обоих ваших ответов, так как @Orace забыл добавить к нему параметр CommandParameter, и ответ из вашего ответа отлично работает. Хотел бы я выбрать оба ваших ответа как правильный. Спасибо!
Привязка для CommandParameter относится к свойству ContextMenu.PlacementTarget
, и это свойство предоставляет элемент, к которому прикреплено ContextMenu. В данном случае это ListView. Вот и все.
@MemeMachine, зачем тебе параметр команды? Чтобы передать ListView
? так вы передаете объект представления в модель представления?! Что вы будете делать с этим?
Как указано в документация, [the] меню [...] зависит от контекста элемента управления.
Другими словами, ContextMenu
имеет тот же контекст данных, что и его родительский элемент управления (здесь ListView
).
Чтобы следовать шаблону MVVM, вам нужно всего лишь добавить ICommand
в контекст данных ListView
и привязать его к свойству MenuItem.Command
.
Вид:
<ListView ItemsSource="{Binding Drives}">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Refresh" Command="{Binding RefreshCommand}" />
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
<GridView>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="Type" DisplayMemberBinding="{Binding Type}"/>
<GridViewColumn Header="Total Size" DisplayMemberBinding="{Binding TotalSize}"/>
<GridViewColumn Header="Free Space" DisplayMemberBinding="{Binding FreeSpace, StringFormat='{}{0:0.00}'}"/>
</GridView>
</ListView.View>
</ListView>
Модель представления:
public class ViewModel
{
public ViewModel()
{
RefreshCommand = new ActionCommand(Refresh);
}
public IReadOnlyList<DriveViewModel> Drives { get; }
public ICommand RefreshCommand { get; }
private void Refresh()
{
// ...
}
}
Доступна рабочая демонстрация здесь.
Если вам действительно нужно запустить команду по событию MouseDown
, а не только при нажатии/выборе пункта меню (через клавиатуру), то ответ здесь. Но это не стандартное поведение, и я не понимаю, почему кто-то может захотеть сделать это таким образом.
Я исправил это, изменив его на Click. MouseDown вообще не работает для ContextMenu. Не уверен, почему это даже доступно как событие.
@MemeMachine I fixed it by changing it to Click
для обработки события Click вам не нужно поддерево Interaction.Triggers\EventTrigger\InvokeCommandAction
, просто используйте атрибуты Command
и CommandParameter
пункта меню. И опять же, я не верю, что вам нужно передавать параметр, поэтому CommandParameter
в вашем случае бесполезен.
Итак,
DataContext
в ListView привязан кTabItemViewModel
, аDrives
является одним из его свойств? Затем вы хотите установить этот ListView в CommandParameter?