0

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

  }
}
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • 1
    Typically you won't want to mix markup and runtime defined items, as the assumption is one or the other; is there any reason you can't just bind these "static" ones the same way as the "dynamic" ones? I mean, all you'd need to do is get the dynamic ones, and prepend and append your static ones either side. – Clint Dec 14 '17 at 15:18
  • @Clint the markup ones are using Bindings for Visiility, Icons and other things - I would have to somehow recreate all that in code - wich pushes the boundary for what is xaml and what is code further away but does not resolve it as they are hooking into others XAML resources and objects.This example is kindof simplified ;) - thanks for suggestions though. – Patrick Artner Dec 14 '17 at 16:18
  • you could just use a data template for that surely? Just ensure your menu item view model has the concept of visibility and icon properties, and then just have your data template bind to them. – Clint Dec 14 '17 at 16:20

1 Answers1

1

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.

see CompositeCollection + CollectionContainer: Bind CollectionContainer.Collection to property of ViewModel that is used as DataTemplates DataType

simple answer:

<CollectionViewSource x:Key="testing" Source="{Binding items}"></CollectionViewSource>

<ContextMenu.ItemsSource>
                <CompositeCollection>
                    <MenuItem Header="Standard MenuItem 3" />
                    <CollectionContainer Collection="{Binding Source={StaticResource testing}}" />
                    <MenuItem Header="Standard MenuItem 6" />
                </CompositeCollection>
            </ContextMenu.ItemsSource>

like this you can add an item into the datacontext.items and it appears in the menu right away

Milan
  • 616
  • 5
  • 11