Почему контекст данных контекстного меню имеет значение null при первом открытии, но то, что ожидается при следующем открытии в WPF?

Поэтому, когда программа запускается и я щелкаю правой кнопкой мыши 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;
    }

Ничего не помогло.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
56
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

private void ListViewItem_ContextMenuOpening(object sender, RoutedEventArgs e)
{
    if (sender is ContextMenu contextMenu &&
        contextMenu.PlacementTarget is FrameworkElement placementTarget)
    {
        contextMenu.DataContext = placementTarget.DataContext;
    }
}

Вы также можете использовать привязку данных, например: stackoverflow.com/a/14438949/1136211

Clemens 01.09.2024 14:45

Вот так: <ContextMenu DataContext = "{Binding PlacementTarget.DataContext, RelativeSource = {RelativeSource Self}}">

Clemens 02.09.2024 13:59

ContextMenu ведет себя немного дико со своим DataContext, когда вы пытаетесь использовать один ContextMenu экземпляр для нескольких потенциальных целей.

Хорошее решение этой проблемы — установить ContextMenuDataContext при открытии. Использование 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.

Clemens 02.09.2024 13:51

Решение с привязкой данных я пробовал уже давно, но результат меня не удовлетворил. В некоторых случаях связывание было слишком медленным. Ведь DispatcherPriority.DataBind не самый высокий. Также с пользовательским классом мне не нужно помнить о таких привязках, потому что все работает так, как ожидалось.

Sinus32 02.09.2024 13:59

Для меня это не имеет смысла. Используете сложный пользовательский элемент управления вместо простого выражения привязки? Не уверен, что именно вам придется запомнить помимо обычного синтаксиса привязки. И «привязка была слишком медленной» — это не совсем точная формулировка проблемы, особенно когда это было «давно».

Clemens 02.09.2024 14:05

«Слишком медленно» означает, что некоторые другие привязки могли жаловаться на nullDataContext до того, как привязка DataContext была разрешена. Об использовании простых привязок: если мне приходится выбирать между использованием простой привязки или вообще ничего не использовать, я выбираю вообще ничего не использовать. Вам нужно написать одну и ту же привязку в каждом экземпляре ContextMenu, написание простого пользовательского элемента управления выполняется только один раз.

Sinus32 02.09.2024 14:29

Вы можете установить привязку в установщике для свойства DataContext в глобальном стиле ContextMenu.

Clemens 02.09.2024 14:38
Ответ принят как подходящий

Просто используйте привязку данных:

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

спасибо, это выглядит как хорошее решение, но я уже нашел решение, которое проще, но вы знаете, что оно включает в себя некоторый код. Крепления просто лучше, спасибо

Xanbaba Fatullayev 02.09.2024 15:26

Я бы не сказал, что прикрепить обработчик ContextMenuOpening на самом деле проще.

Clemens 02.09.2024 15:45

Другие вопросы по теме