Search code examples
c#wpfxamldata-bindingwpf-style

Binding errors when using CompositeCollection as ItemSource with variable types on a MenuItem


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.


Solution

  • 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