I am building a context menu using XAML MenuItems (aka static
) mixed with dynamicly created MenuItems followed by more statics. Some of them are hidden if no dynamics shown, some only shown if dynamics are in.
(I left the Bindings and ValueConverters to hide/show stuff out of the mcve)
Contextmenu:
Static entry 1 // this one is hidden if any dynamic entries are visible
Static entry 2 // always visible
-------------- // seperator, hidden if no dynamic entry is shown
dynamic entries // \_
dynamic entries // \___ shown sorted if any in collection
dynamic entries // _/ and then only those with filter == ok
dynamic entries // /
--------------- // seperator - always visible
Static entry 3 //
Static entry 4 // \ three more static entries,
Static entry 5 // / always visble
2 Problems: Memory goes up and up - and several red XAML errors
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ItemsControl', AncestorLevel='1''. BindingExpression:Path=VerticalContentAlignment; DataItem=null; target element is 'MenuItem' (Name=''); target property is 'VerticalContentAlignment' (type 'VerticalAlignment')
I was unable to directly bind the ICollecionView
to the ContextMenue.ItemSource.CompositeCollection.CollectionContainer.Collection
in a way that it autoupdated on ObersveableCollection
-changes that the view uses as its source.
Thats why I used the INotifyPropertyChanged
from the ObersveableCollection
-Items
to circumvent that - my guess is that I get dangling EventListeners by resetting the CollectionContainer
's Collection
to the recreated ICollectionView.
How to solve it properly without errors and ever increasing memory?
"Minimal" Example Code: (from WPF App (.NET Framework)
template)
MainWindow.xaml:
<Window x:Class="DarnContextMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DarnContextMenu"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ResourceDictionary>
<!-- Vm to MenuItem -->
<local:VmToMenuItemConverter x:Key="VmToMenuItem"/>
<!-- display template -->
<DataTemplate x:Key="vmTemplate">
<StackPanel Margin="5">
<TextBlock Text="{Binding ConName}"/>
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<Window.ContextMenu>
<ContextMenu>
<ContextMenu.ItemsSource>
<CompositeCollection >
<!-- Connectoptions -->
<MenuItem Header="Connect to last used"/>
<MenuItem Header="Connect to ..."/>
<Separator/>
<!-- List of not disabled connections -->
<CollectionContainer x:Name="cc" Collection="{Binding ConsView, Converter={StaticResource VmToMenuItem}}"/>
<Separator/>
<!-- Others -->
<MenuItem Header="Settings ..."/>
<MenuItem Header="Do Something ..."/>
<MenuItem Header="Exit ..."/>
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>
</Window.ContextMenu>
<DockPanel>
<Label DockPanel.Dock="Bottom" x:Name="msgBlock" Height="28" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="0,5" HorizontalAlignment="Center">Listview with ICollectionView</TextBlock>
<ListView Grid.Column="0" ItemsSource="{Binding ConsView}" ItemTemplate="{StaticResource vmTemplate}" Background="LightGray"/>
</DockPanel>
<DockPanel Grid.Column="2">
<TextBlock DockPanel.Dock="Top" Margin="0,5" HorizontalAlignment="Center">Listview with ObservableCollection:</TextBlock>
<ListView ItemsSource="{Binding Cons}" ItemTemplate="{StaticResource vmTemplate}"/>
</DockPanel>
</Grid>
</DockPanel>
</Window>
MainWindow.xaml.cs (all squashed together for MCVE):
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
namespace DarnContextMenu
{
// States used for filtering what is displayed via ICollectionView
public enum EConState { Disabled, LoggedIn, LoggedOff };
// Stripped down model
public class Connection
{
public Connection (string name)
{
Name = name;
}
public EConState State { get; set; } = EConState.Disabled;
public string Name { get; set; } = string.Empty;
}
// Viewmodel
public class ConnectionVM : DependencyObject, INotifyPropertyChanged
{
// Simulation of changing States
static List<EConState> allStates = new List<EConState> { EConState.Disabled, EConState.LoggedIn, EConState.LoggedOff };
Timer t;
void changeMe (object state)
{
if (state is ConnectionVM c)
MainWindow.UIDispatcher
.Invoke (() => c.State =
allStates
.Where (s => s != c.State)
.OrderBy (_ => Guid.NewGuid ().GetHashCode ())
.First ());
}
// End of simulation of changing States
public static readonly DependencyProperty StateProperty = DependencyProperty.Register ("State", typeof (EConState), typeof (ConnectionVM),
new PropertyMetadata (EConState.Disabled, (DependencyObject d, DependencyPropertyChangedEventArgs e) =>
{
if (d is ConnectionVM vm)
{
vm.ConName = $"{vm.Connection.Name} [{(EConState)e.NewValue}]";
vm.PropertyChanged?.Invoke (vm, new PropertyChangedEventArgs (nameof (vm.State)));
}
}));
// The state of the connection: influences if the connection is shown at all and used in sorting
public EConState State
{
get { return (EConState)GetValue (StateProperty); }
set { SetValue (StateProperty, value); }
}
// name created by models basename and state - changes via callback from StateProperty
protected static readonly DependencyPropertyKey ConNamePropertyKey = DependencyProperty.RegisterReadOnly ("ConName", typeof (string), typeof (ConnectionVM), new PropertyMetadata (""));
public static readonly DependencyProperty ConNameProperty = ConNamePropertyKey.DependencyProperty;
public string ConName
{
get { return (string)GetValue (ConNameProperty); }
protected set { SetValue (ConNamePropertyKey, value); }
}
Connection Connection { get; }
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
///
/// </summary>
/// <param name="connection">The connection - used for name and initial state</param>
/// <param name="delay">a delay for the timer until the state-changes start</param>
/// <param name="period">a delay between state changes </param>
public ConnectionVM (Connection connection, TimeSpan delay, TimeSpan period)
{
t = new Timer (changeMe, this, (int)delay.TotalMilliseconds, (int)period.TotalMilliseconds);
Connection = connection;
State = Connection.State; // changing, simulated by timer inside VM
}
}
public class MainViewModel
{
// all connections - in RL: occasionally new ones will be added by the user
public ObservableCollection<ConnectionVM> Cons { get; set; }
// filtered and sorted view on Cons - Collection
public ICollectionView ConsView { get; set; }
public MainViewModel (CollectionContainer cc)
{
// demodata - normally connections are created by userinteractions
// this simulates 9 connections that change status every 4s to 10s
Cons = new ObservableCollection<ConnectionVM> (
Enumerable.Range (1, 9)
.Select (n => new ConnectionVM (new Connection ($"Connection #{n}")
, TimeSpan.FromMilliseconds (300 * n)
, TimeSpan.FromMilliseconds (700 * (n + 5))))
);
// create a sorted and filtered view
// - sort by Status and then by Name
// - show only Connecitons that are not Disabled
ConsView = new CollectionViewSource { Source = Cons }.View;
using (var def = ConsView.DeferRefresh ())
{
ConsView.SortDescriptions.Add (new SortDescription ("State", ListSortDirection.Ascending));
ConsView.SortDescriptions.Add (new SortDescription ("ConName", ListSortDirection.Ascending));
ConsView.Filter = obj => (obj is ConnectionVM vm) && vm.State != EConState.Disabled;
}
// attach a Refresh-Action of MVM to each ConnectionVMs PropertyChanged which is fired by
// ConnectionVM.StateProperty.Callback notifies each listener on StateProperty-Change
foreach (var vm in Cons)
{
vm.PropertyChanged += (s, e) => // object s, PropertyChangedEventArgs e
{
cc.Collection = ConsView;
RefreshViewModels ();
};
}
// in case the whole collection is added or removed to/from
Cons.CollectionChanged += (s, e) =>
{
cc.Collection = ConsView;
RefreshViewModels ();
};
}
void RefreshViewModels ()
{
ConsView.Refresh ();
MainWindow.logger.Content = $"Valid: {Cons.Count (c => c.State != EConState.Disabled)}/{Cons.Count ()} (In/Off/Disabled: {Cons.Count (c => c.State == EConState.LoggedIn)} / {Cons.Count (c => c.State == EConState.LoggedOff)} / {Cons.Count (c => c.State == EConState.Disabled)})";
}
}
// create a MenuItem from the ConnectionVM - in real theres a bit more code inside due to Icons, Commands, etc.
public class VmToMenuItemConverter : IValueConverter
{
public object Convert (object value, Type targetType, object parameter, CultureInfo culture)
=> new MenuItem { Header = (value as ConnectionVM).ConName ?? $"Invalid '{value.GetType ()}'" };
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) => null;
}
public partial class MainWindow : Window
{
public static Dispatcher UIDispatcher = null;
public static Label logger = null;
public MainWindow ()
{
UIDispatcher = Application.Current.Dispatcher;
InitializeComponent ();
logger = msgBlock;
DataContext = new MainViewModel (cc);
}
}
}