2

In simple terms, I have a WPF MenuItem with a list of recent files that have been opened, that uses a class named "RecentFilesViewModel" to populate the files and setup the commands for them to open. But the problem comes when I add a Seperator and a final manually added MenuItem that clears the recent files list.

My problem is, while using a CompositeCollection to set the ItemSource, it works fine with the CollectionContainer of my recent files list provided by a custom class, but as soon as I include the Seperator or clear files MenuItem I get binding issues. Annoyingly it does actually work just as expected with no issues, but I really want to understand why the binding errors are showing, and just get rid of them.

Here is my XAML for the MenuItem and it's CompositeCollection:

<MenuItem Header="_Recent files">
    <MenuItem.ItemsSource>
        <CompositeCollection>
            <CollectionContainer Collection="{Binding Source={StaticResource recentFilesViewModel}, Path=RecentFiles}" />
            <Separator Name="Seperator" />
            <MenuItem Name="ClearRecentFilesButton" Header="Clear recent files" Command="{x:Static local:ApplicationMenuHandler.File_RecentFiles_Clear}" />
        </CompositeCollection>
    </MenuItem.ItemsSource>
    <MenuItem.ItemContainerStyle>
        <Style TargetType="MenuItem">
            <Style.Triggers>
                <DataTrigger Value="{x:Null}">
                    <DataTrigger.Binding>
                        <PriorityBinding>
                            <Binding Path="Command"/>
                        </PriorityBinding>
                    </DataTrigger.Binding>
                    <Setter Property="Command" Value="{x:Static local:ApplicationMenuHandler.File_RecentFiles_Open}"/>
                    <Setter Property="CommandParameter" Value="{Binding FilePath}"/>
                    <Setter Property="Header" Value="{Binding FilePath}"/>
                    <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </MenuItem.ItemContainerStyle>
</MenuItem>

After removing the lines:

<Separator Name="Seperator" />
<MenuItem Name="ClearRecentFilesButton" Header="Clear recent files" Command="{x:Static local:ApplicationMenuHandler.File_RecentFiles_Clear}" />

I get no binding errors at all. So what is causing the errors? I would have thought that the CompositeCollection allows for exactly that, a composite collection of variable types?

Some things to note are:

  1. When adding just the Seperator to the collection, the binding error only shows AFTER I click on one of the contained menu items. Here is the error:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ItemsControl', AncestorLevel='1''. BindingExpression:Path=HorizontalContentAlignment; DataItem=null; target element is 'MenuItem' (Name=''); target property is 'HorizontalContentAlignment' (type 'HorizontalAlignment')

  1. When adding just the extra MenuItem, the error shows as soon as the application loads. But is basically the same error:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ItemsControl', AncestorLevel='1''. BindingExpression:Path=HorizontalContentAlignment; DataItem=null; target element is 'MenuItem' (Name='ClearRecentFilesButton'); target property is 'HorizontalContentAlignment' (type 'HorizontalAlignment')

I have have gone around in circles trying to solve it, I wondered if it had something to do with the DataTrigger, but after trying many different ways of targeting only MenuItems that have a "Command" attribute, nothing seemed to change anything. Maybe I am missunderstanding how the DataTrigger works, I really wish I could just use the code behind as this seems so unnecessarily complicated to achieve something so simple if it was code and not XAML markup.

Really would appreciate any help at all, and I'm very grateful for any help! Thank you in advance.


Update (as requested by davmos)

@davmos suggested that I added some more info regarding the ApplicationMenuHandler and RecentFilesViewModel classes, and also where I instantiate it etc.

The ApplicationMenuHandler is simply a set of static commands, all seem to be working fine, here is an example of the beginning of the class itself:

public static class ApplicationMenuHandler
{
    // File -> New
    public static ICommand File_New { get; } = new RelayCommand((parameter) => {
        // Create new level editor
        LevelEditor levelEditor = new(){ Tag = "New Level.lvl (" + MainWindow.instance?.actionTabsModel.Tabs.Count + ")" };

        // Add a tab with the new level editor
        MainWindow.instance?.actionTabsModel.AddTab(levelEditor);

        // Once level editor is loaded and ready to scroll, scroll to center
        SubRoutines.WaitUntil(() => levelEditor.levelScrollViewer != null && levelEditor.levelScrollViewer.IsLoaded, () => {
            levelEditor.levelScrollViewer.ScrollToCenter();
        });
    });

And here is the entire RecentFileViewModel class which extends ViewModelBase (which simply has a PropertyChangedEventHandler and NotifyPropertyChanged method). Here is the class:

namespace LevelXEditor.Project.RecentFiles
{
    public class RecentFilesViewModel : ViewModelBase
    {
        public static RecentFilesViewModel? Instance { get; private set; }

        private ObservableCollection<RecentFile> recentFiles = new();
        public ObservableCollection<RecentFile> RecentFiles { get => recentFiles; set { recentFiles = value; NotifyPropertyChanged("RecentFiles"); } }

        // Constructor
        public RecentFilesViewModel()
        {
            Instance = this;
            RecentFiles = new ObservableCollection<RecentFile>();
            Refresh();
        }

        public void Refresh()
        {
            // Clear recent files
            RecentFiles.Clear();

            // Add recent files
            foreach (string recentFile in MainWindow.AppDataHandler.Data.recentFiles)
            {
                RecentFiles.Add(new RecentFile() { FilePath = recentFile, IsEnabled = true });
            }

            // If there are no recent files then add a placeholder
            if (RecentFiles.Count == 0)
            {
                RecentFiles.Add(new RecentFile() { FilePath = "No recent files", IsEnabled = false });
            }
        }
    }

    public class RecentFile
    {
        public string FilePath { get; set; } = "";
        public bool IsEnabled { get; set; } = true;
    }
}

I'm still coming to understand how WPF works on the XAML side of things, but this is how I am "instantiating" the RecentFilesViewModel object in my MainWindow XAML:

<Window x:Class="LevelXEditor.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:LevelXEditor"
        xmlns:recentFiles="clr-namespace:LevelXEditor.Project.RecentFiles"
        xmlns:statusBar="clr-namespace:LevelXEditor.Project.StatusBar"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="600" Width="1000">
    <Window.Resources>
        <statusBar:StatusBarViewModal x:Key="statusBarViewModel"/>
        <recentFiles:RecentFilesViewModel x:Key="recentFilesViewModel"/>
        <local:EqualConverter x:Key="EqualConverter" />
    </Window.Resources>

Hopefully this gives a little more helpful information.

davmos
  • 9,324
  • 4
  • 40
  • 43
Zephni
  • 753
  • 1
  • 7
  • 26

1 Answers1

1

Seems to be known issue that can be worked around by overriding 2 properties in your App.xaml file...

<Application.Resources>
    <Style TargetType="MenuItem">
        <Setter Property="HorizontalContentAlignment" Value="Left"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
    </Style>
</Application.Resources>

If you want to dig deeper, see answer below & the links to MSDN support forum threads within it...

MenuItem added programmatically causes Binding error

davmos
  • 9,324
  • 4
  • 40
  • 43
  • Hi @davmos, thanks for the answer. I tried adding the two setters within the MenuItem Style, and after the StyleTriggers tags, but I am sadly still getting the same binding errors :( – Zephni Apr 01 '23 at 18:13
  • 1
    OK @Zephni, can you share a [minimal repro app](https://stackoverflow.com/help/minimal-reproducible-example) or can you add more code to the question?... e.g. `ApplicationMenuHandler` class; the `RecentFilesViewModel` class & resource declaration & where you instantiate it. – davmos Apr 01 '23 at 22:05
  • Hi @davmos, and thanks, I've added some extra information to the question. When you mention "resource declaration" and where I instantiate it. This may be where I am getting confused, as I am used to instantiating objects in standard C#, but in WPF it seems you are meant to "instantiate" it in the XAML, and still haven't quite got my head around that yet. – Zephni Apr 03 '23 at 18:53
  • 1
    Hi @Zephni, thanks for the code. I was able to reproduce your 2. error on app load & got rid of it by overriding those properties in `Application.Resources` in App.xaml. I've updated the answer & linked to a different question/answer for more info. – davmos Apr 03 '23 at 21:56
  • That's fantastic thanks so much for the extra effort as well, really appreciate that! That has removed the binding warnings! I do wonder why that has to be an Application Resource and can't be defined as the MenuItem Style inline like we were trying before. But thanks so much, I can finally sleep tonight – Zephni Apr 04 '23 at 08:21
  • 1
    Also why haven't they fixed this bug after so many years?! Microshafted again!! The main reason I asked for more code was to make it easier to create a minimal app to reproduce the issue, test solutions & ultimately give you a better answer, sooner. Glad you can now sleep tonight – davmos Apr 04 '23 at 08:57