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:
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')
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.
@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.
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...