Поэтому, когда программа запускается и я щелкаю правой кнопкой мыши ListViewItem, отображается контекстное меню, но его DataContext не является объектом, связанным с ListViewItem, который я щелкнул. Вот почему эта привязка не работает и метод Convert конвертера не запускается:
<Image>
<Image.Source>
<Binding
Path = "Process" Converter = "{StaticResource PriorityToIconConverter}"
ConverterParameter = "{StaticResource RealTime}" />
</Image.Source>
</Image>
Но когда я закрываю ContextMenu и открываю его снова, внезапно все работает нормально. Но почему не сразу после запуска программы?
У меня есть этот ListView:
<ListView Grid.Row = "1" x:Name = "ListView" Margin = "0,0,0,10"
ScrollViewer.CanContentScroll = "True"
ScrollViewer.VerticalScrollBarVisibility = "Auto"
ItemsSource = "{Binding ProcessInfos}"
Background = "#1a1a1a" Foreground = "#e0e0e0"
BorderBrush = "#333333" BorderThickness = "1"
SelectionMode = "Single"
SelectedItem = "{Binding SelectedProcessInfo, UpdateSourceTrigger=PropertyChanged}"
FontSize = "15">
<ListView.Resources>
<ControlTemplate x:Key = "CustomHeader" TargetType = "GridViewColumnHeader">
<Grid MinWidth = "50" Background = "{TemplateBinding Background}" Height = "40">
<TextBlock Background = "{TemplateBinding Background}" TextAlignment = "Center"
Foreground = "{TemplateBinding Foreground}"
Text = "{TemplateBinding Tag}" VerticalAlignment = "Center" />
<Thumb x:Name = "PART_HeaderGripper" HorizontalAlignment = "Right" Margin = "0" Width = "1" />
</Grid>
</ControlTemplate>
<Style TargetType = "GridViewColumnHeader">
<Style.Setters>
<Setter Property = "Background" Value = "#2b2b2b" />
<Setter Property = "Foreground" Value = "#e0e0e0" />
<Setter Property = "FontWeight" Value = "Bold" />
<Setter Property = "BorderBrush" Value = "#333333" />
<Setter Property = "BorderThickness" Value = "2" />
<Setter Property = "Padding" Value = "5" />
</Style.Setters>
<Style.Triggers>
<Trigger Property = "IsMouseOver" Value = "True">
<Setter Property = "Background" Value = "#3a3a3a" />
<Setter Property = "Foreground" Value = "#ffffff" />
</Trigger>
<Trigger Property = "IsPressed" Value = "True">
<Setter Property = "Background" Value = "#4a4a4a" />
<Setter Property = "Foreground" Value = "#e0e0e0" />
</Trigger>
</Style.Triggers>
</Style>
</ListView.Resources>
<ListView.View>
<GridView d:DataContext = "{d:DesignInstance Type=models:ProcessInfo}">
<GridViewColumn Width = "200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid Margin = "5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border>
<Image Source = "{Binding Icon}" Width = "20" Height = "20" />
</Border>
<TextBlock Grid.Column = "1" Text = "{Bindin gProcess.ProcessName}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
<GridViewColumn.Header>
<GridViewColumnHeader Template = "{StaticResource CustomHeader}" Margin = "-3,0,0,0"
BorderThickness = "0" Tag = "Process Name" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width = "100"
DisplayMemberBinding = "{Binding Process.Id}">
<GridViewColumn.Header>
<GridViewColumnHeader Template = "{StaticResource CustomHeader}" Margin = "-3,0,0,0"
BorderThickness = "0" Tag = "PID" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width = "180"
DisplayMemberBinding = "{Binding MemoryUsage}">
<GridViewColumn.Header>
<GridViewColumnHeader Template = "{StaticResource CustomHeader}" Margin = "-3,0,0,0"
BorderThickness = "0" Tag = "Memory Usage" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width = "180"
DisplayMemberBinding = "{Binding CpuUsage}">
<GridViewColumn.Header>
<GridViewColumnHeader Template = "{StaticResource CustomHeader}" Margin = "-3,0,0,0"
BorderThickness = "0" Tag = "CPU Usage" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width = "120"
DisplayMemberBinding = "{Binding Process.Threads.Count}">
<GridViewColumn.Header>
<GridViewColumnHeader Template = "{StaticResource CustomHeader}" Margin = "-3,0,0,0"
BorderThickness = "0" Tag = "Thread Count" />
</GridViewColumn.Header>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
И это ContextMenu ListViewItem:
<ContextMenu x:Key = "MenuItemContextMenu" Opened = "ListViewItem_ContextMenuOpening" Background = "#1e1e1e" Foreground = "#e0e0e0" BorderBrush = "#4a4a4a"
d:DataContext = "{d:DesignInstance models:ProcessInfo}">
<ContextMenu.Resources>
<ControlTemplate x:Key = "MenuItemTemplate" TargetType = "MenuItem">
<Border Background = "{TemplateBinding Background}"
BorderBrush = "{TemplateBinding BorderBrush}"
BorderThickness = "0" Padding = "25 10">
<Grid>
<ContentPresenter ContentSource = "Header" HorizontalAlignment = "Left"
VerticalAlignment = "Center" Margin = "5,0" />
<ContentPresenter x:Name = "Icon" ContentSource = "Icon"
HorizontalAlignment = "Right"
Width = "5"
Height = "5"
VerticalAlignment = "Center" Margin = "0 0 -10 0" />
<Popup x:Name = "SubMenuPopup" Placement = "Right"
IsOpen = "{TemplateBinding IsSubmenuOpen}" Focusable = "False"
PopupAnimation = "Fade">
<Border Background = "#1e1e1e" BorderBrush = "#4a4a4a"
BorderThickness = "1">
<StackPanel IsItemsHost = "True" KeyboardNavigation.DirectionalNavigation = "Cycle" />
</Border>
</Popup>
</Grid>
</Border>
</ControlTemplate>
<!-- Style for MenuItems -->
<Style TargetType = "MenuItem">
<Setter Property = "Background" Value = "#1e1e1e" />
<Setter Property = "Foreground" Value = "#e0e0e0" />
<Setter Property = "Template" Value = "{StaticResource MenuItemTemplate}">
</Setter>
<Style.Triggers>
<Trigger Property = "IsMouseOver" Value = "True">
<Setter Property = "Background" Value = "#333333" />
<Setter Property = "Foreground" Value = "#ffffff" />
</Trigger>
<Trigger Property = "IsPressed" Value = "True">
<Setter Property = "Background" Value = "#444444" />
<Setter Property = "Foreground" Value = "#ffffff" />
</Trigger>
<Trigger Property = "IsSubmenuOpen" Value = "True">
<Setter Property = "Background" Value = "#333333" />
</Trigger>
</Style.Triggers>
</Style>
<diagnostics:ProcessPriorityClass x:Key = "RealTime">RealTime</diagnostics:ProcessPriorityClass>
<diagnostics:ProcessPriorityClass x:Key = "High">High</diagnostics:ProcessPriorityClass>
<diagnostics:ProcessPriorityClass x:Key = "AboveNormal">AboveNormal</diagnostics:ProcessPriorityClass>
<diagnostics:ProcessPriorityClass x:Key = "Normal">Normal</diagnostics:ProcessPriorityClass>
<diagnostics:ProcessPriorityClass x:Key = "BelowNormal">BelowNormal</diagnostics:ProcessPriorityClass>
<diagnostics:ProcessPriorityClass x:Key = "Low">Idle</diagnostics:ProcessPriorityClass>
<converters:PriorityToIconConverter x:Key = "PriorityToIconConverter" />
</ContextMenu.Resources>
<!-- Set Affinity -->
<MenuItem Header = "Set affinity" />
<!-- Set Priority with Submenu -->
<MenuItem Header = "Set priority" x:Name = "MenuItem">
<MenuItem Header = "Realtime" CommandParameter = "{StaticResource RealTime}">
<MenuItem.Icon>
<Image>
<Image.Source>
<Binding
Path = "Process" Converter = "{StaticResource PriorityToIconConverter}"
ConverterParameter = "{StaticResource RealTime}" />
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header = "High" CommandParameter = "{StaticResource High}">
<MenuItem.Icon>
<Image>
<Image.Source>
<Binding
Path = "Process" Converter = "{StaticResource PriorityToIconConverter}"
ConverterParameter = "{StaticResource High}" />
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header = "Above Normal" CommandParameter = "{StaticResource AboveNormal}">
<MenuItem.Icon>
<Image>
<Image.Source>
<Binding
Path = "Process" Converter = "{StaticResource PriorityToIconConverter}"
ConverterParameter = "{StaticResource AboveNormal}" />
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header = "Normal" CommandParameter = "{StaticResource Normal}">
<MenuItem.Icon>
<Image>
<Image.Source>
<Binding
Path = "Process" Converter = "{StaticResource PriorityToIconConverter}"
ConverterParameter = "{StaticResource Normal}" />
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header = "Below Normal" CommandParameter = "{StaticResource BelowNormal}">
<MenuItem.Icon>
<Image>
<Image.Source>
<Binding
Path = "Process" Converter = "{StaticResource PriorityToIconConverter}"
ConverterParameter = "{StaticResource BelowNormal}" />
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header = "Low" CommandParameter = "{StaticResource Low}">
<MenuItem.Icon>
<Image>
<Image.Source>
<Binding
Path = "Process" Converter = "{StaticResource PriorityToIconConverter}"
ConverterParameter = "{StaticResource Low}" />
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</ContextMenu>
<Style TargetType = "ListViewItem">
<Setter Property = "Background" Value = "#1e1e1e" />
<Setter Property = "BorderBrush" Value = "#333333" />
<Setter Property = "BorderThickness" Value = "0,0,0,1" />
<Setter Property = "Padding" Value = "10" />
<Setter Property = "Margin" Value = "0,0,0,5" />
<Setter Property = "Foreground" Value = "#e0e0e0" />
<Setter Property = "ContextMenu" Value = "{StaticResource MenuItemContextMenu}" />
<!-- Hover and Selected States -->
<Style.Triggers>
<Trigger Property = "IsMouseOver" Value = "True">
<Setter Property = "Background" Value = "#333333" />
<Setter Property = "Foreground" Value = "#ffffff" />
</Trigger>
<Trigger Property = "IsSelected" Value = "True">
<Setter Property = "Background" Value = "#444444" />
<Setter Property = "Foreground" Value = "#ffffff" />
</Trigger>
</Style.Triggers>
</Style>
Модель просмотра:
using System.Diagnostics;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using TaskManagerPlusPlus.Dialogs;
using TaskManagerPlusPlus.Models;
using TaskManagerPlusPlus.Services;
using TaskManagerPlusPlus.Views;
using Timer = System.Timers.Timer;
namespace TaskManagerPlusPlus.ViewModels;
public partial class TasksViewModel : BaseViewModel
{
private readonly IProcessInfosManager _processInfosManager;
[ObservableProperty] private ProcessInfosCollection _processInfos = [];
[ObservableProperty] private ProcessInfo? _selectedProcessInfo;
public TasksViewModel(IProcessInfosManager processInfosManager)
{
_processInfosManager = processInfosManager;
RefreshProcesses();
var timer = new Timer(1000)
{
AutoReset = true
};
timer.Elapsed += (_, _) => { RefreshProcesses(); };
timer.Start();
}
private void RefreshProcesses()
{
var selectedProcessInfoProcessId = SelectedProcessInfo?.Process.Id;
try
{
var processInfos = _processInfosManager.GetProcessInfos();
ProcessInfos.Clear();
foreach (var processInfo in processInfos)
{
ProcessInfos.Add(processInfo);
}
Application.Current.Dispatcher.Invoke(() =>
{
ProcessInfos.NotifyCollectionChanged();
SelectedProcessInfo = ProcessInfos.FirstOrDefault(processInfo =>
processInfo.Process.Id == selectedProcessInfoProcessId);
});
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
[RelayCommand]
private void KillProcess()
{
SelectedProcessInfo?.Process.Kill();
}
[RelayCommand]
private void StartProcess()
{
try
{
var startProcessDialog = new StartProcessDialog();
var showDialog = startProcessDialog.ShowDialog();
if (showDialog.HasValue && showDialog.Value)
{
var process = new Process
{
StartInfo = new ProcessStartInfo(startProcessDialog.TaskLocation)
{
ErrorDialog = true
}
};
process.Start();
}
}
catch (Exception)
{
MessageBox.Show("Process cannot be started.", "Process startup failure", MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
}
Я пробовал это:
public TasksView()
{
InitializeComponent();
var timer = new Timer(2000) { AutoReset = true };
timer.Elapsed += (_, _) =>
{
try
{
var listViewItem = (ListViewItem)ListView.ItemContainerGenerator.ContainerFromItem(ListView.Items[0]);
Dispatcher.Invoke(() =>
{
var contextMenu = listViewItem.ContextMenu!;
var menuItem = (MenuItem)((MenuItem)contextMenu.Items[1]!).Items[3]!;
Console.WriteLine(contextMenu.DataContext == null);
});
}
catch (Exception e)
{
Console.WriteLine(e);
}
};
timer.Start();
}
Он показал, что при запуске программы контекст данных имеет значение null, но затем после закрытия и повторного открытия он показывает, что он не равен нулю.
Я также попробовал это:
private void TasksView_OnLoaded(object sender, RoutedEventArgs e)
{
var listViewItem = (ListViewItem)ListView.ItemContainerGenerator.ContainerFromItem(ListView.Items[0]);
var contextMenu = listViewItem.ContextMenu!;
contextMenu.IsOpen = true;
contextMenu.IsOpen = false;
}
Ничего не помогло.
private void ListViewItem_ContextMenuOpening(object sender, RoutedEventArgs e)
{
if (sender is ContextMenu contextMenu &&
contextMenu.PlacementTarget is FrameworkElement placementTarget)
{
contextMenu.DataContext = placementTarget.DataContext;
}
}
Вот так: <ContextMenu DataContext = "{Binding PlacementTarget.DataContext, RelativeSource = {RelativeSource Self}}">
ContextMenu
ведет себя немного дико со своим DataContext
, когда вы пытаетесь использовать один ContextMenu
экземпляр для нескольких потенциальных целей.
Хорошее решение этой проблемы — установить ContextMenu
DataContext
при открытии. Использование PlacementTarget
в качестве источника права DataContext
— хорошая идея, поэтому и ваше решение хорошее.
Другое решение — изменить DataContext
, когда PlacementTarget
изменится. Вы можете использовать собственный класс ContextMenu
, чтобы вам не приходилось иметь дело с обработчиками событий.
public class SharedContextMenu : ContextMenu
{
private readonly DependencyPropertyChangedEventHandler _elementDataContextChanged;
static SharedContextMenu()
{
PlacementTargetProperty.OverrideMetadata(
forType: typeof(SharedContextMenu),
typeMetadata: new FrameworkPropertyMetadata(
propertyChangedCallback: PlacementTargetChanged));
}
public SharedContextMenu()
{
_elementDataContextChanged = ElementDataContextChanged;
}
private static void PlacementTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = (SharedContextMenu)d;
var oldElement = e.OldValue as FrameworkElement;
if (oldElement != null)
{
oldElement.DataContextChanged -= self._elementDataContextChanged;
}
var newElement = e.NewValue as FrameworkElement;
if (newElement != null)
{
self.DataContext = newElement.DataContext;
newElement.DataContextChanged += self._elementDataContextChanged;
}
else
{
self.DataContext = null;
}
}
private void ElementDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
DataContext = e.NewValue;
}
}
Замените обычный <ContextMenu>
на этот, и все должно работать.
Вам не нужен собственный ContextMenu. При привязке данных (как показано в комментарии к другому ответу) DataContext ContextMenu обновляется при изменении PlacementTarget или PlacementTarget DataContext.
Решение с привязкой данных я пробовал уже давно, но результат меня не удовлетворил. В некоторых случаях связывание было слишком медленным. Ведь DispatcherPriority.DataBind
не самый высокий. Также с пользовательским классом мне не нужно помнить о таких привязках, потому что все работает так, как ожидалось.
Для меня это не имеет смысла. Используете сложный пользовательский элемент управления вместо простого выражения привязки? Не уверен, что именно вам придется запомнить помимо обычного синтаксиса привязки. И «привязка была слишком медленной» — это не совсем точная формулировка проблемы, особенно когда это было «давно».
«Слишком медленно» означает, что некоторые другие привязки могли жаловаться на null
DataContext
до того, как привязка DataContext
была разрешена. Об использовании простых привязок: если мне приходится выбирать между использованием простой привязки или вообще ничего не использовать, я выбираю вообще ничего не использовать. Вам нужно написать одну и ту же привязку в каждом экземпляре ContextMenu
, написание простого пользовательского элемента управления выполняется только один раз.
Вы можете установить привязку в установщике для свойства DataContext в глобальном стиле ContextMenu.
Просто используйте привязку данных:
<ContextMenu ...
DataContext = "{Binding PlacementTarget.DataContext,
RelativeSource = {RelativeSource Self}}">
Если вам придется установить эту привязку для нескольких ContextMenus, объявите соответствующий сеттер в глобальном стиле, например
<Window.Resources>
<Style TargetType = "ContextMenu">
<Setter Property = "DataContext"
Value = "{Binding PlacementTarget.DataContext,
RelativeSource = {RelativeSource Self}}"/>
</Style>
</Window.Resources>
спасибо, это выглядит как хорошее решение, но я уже нашел решение, которое проще, но вы знаете, что оно включает в себя некоторый код. Крепления просто лучше, спасибо
Я бы не сказал, что прикрепить обработчик ContextMenuOpening на самом деле проще.
Вы также можете использовать привязку данных, например: stackoverflow.com/a/14438949/1136211