Search code examples
c#wpfxamldata-bindingbinding

Why is Context Menu Data Context is null on the first opening but is what is expected on the next opening in WPF?


So when the program starts and I right-click the ListViewItem the context menu is shown but its DataContext is not the object linked to the ListViewItem that I clicked. That is why this binding does not work and the converter's Convert method not fired:

<Image>
       <Image.Source>
              <Binding
               Path="Process" Converter="{StaticResource PriorityToIconConverter}"
                                    ConverterParameter="{StaticResource RealTime}" />
        </Image.Source>
</Image>

But when I close the ContextMenu and open it again suddenly eerything works fine. But why not right after the start of the program?

I have this 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>

And this ContextMenu of the 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>

ViewModel:

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);
        }
    }
}

I have tried this:

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();
    }

It showed that when the program started the data context is null but then after closing and opening again it shows that it is not null.

I also tried this:

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

Nothing worked.


Solution

  • Just use data binding:

    <ContextMenu ...
        DataContext="{Binding PlacementTarget.DataContext,
                      RelativeSource={RelativeSource Self}}">
    

    In case you would have to set this Binding on multiple ContextMenus, declare an appropriate Setter in a global Style like

    <Window.Resources>
        <Style TargetType="ContextMenu">
            <Setter Property="DataContext"
                    Value="{Binding PlacementTarget.DataContext,
                            RelativeSource={RelativeSource Self}}"/>
        </Style>
    </Window.Resources>