C# ObservableCollection(MCVE)的ICollectionView上的静态/动态混合上下文菜单

C# ObservableCollection(MCVE)的ICollectionView上的静态/动态混合上下文菜单,c#,wpf,xaml,contextmenu,.net-4.6.1,C#,Wpf,Xaml,Contextmenu,.net 4.6.1,我正在使用XAML MenuItems(又名static)与动态创建的MenuItems混合构建上下文菜单,然后使用更多的静态。如果未显示动力学,则其中一些将隐藏,而一些仅在动力学处于中时显示 (我让绑定和值转换器隐藏/显示mcve之外的内容) 上下文菜单: Static entry 1 // this one is hidden if any dynamic entries are visible Static entry 2 // always visible -

我正在使用XAML MenuItems(又名
static
)与动态创建的MenuItems混合构建上下文菜单,然后使用更多的静态。如果未显示动力学,则其中一些将隐藏,而一些仅在动力学处于中时显示

(我让绑定和值转换器隐藏/显示mcve之外的内容)

上下文菜单:

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个问题:内存不断增加-以及几个红色XAML错误

System.Windows.Data错误:4:找不到引用为“RelativeSource FindAncestor,AncestorType='System.Windows.Controls.ItemsControl',AncestorLevel='1'的绑定源。BindingExpression:Path=VerticalContentAlignment;DataItem=null;目标元素是“MenuItem”(名称=“”);目标属性为“VerticalContentAlignment”(类型为“VerticalAlignment”)

我无法直接将
ICollecionView
绑定到
ContextMenue.ItemSource.CompositeCollection.CollectionContainer.Collection
,它会自动更新视图用作其源的
ObersveableCollection
-更改

这就是为什么我使用
ObersveableCollection
-
Items
中的
INotifyPropertyChanged
来规避这一问题-我猜我通过将
CollectionContainer
集合
重置为重新创建的ICollectionView来获得悬挂事件侦听器

如何在没有错误和不断增加内存的情况下正确解决它

“最小”示例代码:(来自WPF应用程序(.NET框架)模板)

MainWindow.xaml:


具有ICollectionView的Listview
具有ObservableCollection的Listview:
MainWindow.xaml.cs(对于MCVE,所有文件都压缩在一起):

使用系统;
使用System.Collections.Generic;
使用System.Collections.ObjectModel;
使用系统组件模型;
利用制度全球化;
使用System.Linq;
使用系统线程;
使用System.Windows;
使用System.Windows.Controls;
使用System.Windows.Data;
使用System.Windows.Threading;
名称空间DarnContext菜单
{
//用于过滤通过ICollectionView显示内容的状态
公共枚举EConState{Disabled,LoggedIn,LoggedOff};
//简化模型
公共类连接
{
公共连接(字符串名称)
{
名称=名称;
}
公共EConState状态{get;set;}=EConState.Disabled;
公共字符串名称{get;set;}=string.Empty;
}
//视图模型
公共类ConnectionVM:DependencyObject,INotifyPropertyChanged
{
//变化状态的模拟
静态列表allStates=新列表{EConState.Disabled,EConState.LoggedIn,EConState.LoggedOff};
定时器t;
void changeMe(对象状态)
{
if(状态为ConnectionVM c)
MainWindow.UIDispatcher
.Invoke(()=>c.State=
各州
.其中(s=>s!=c.State)
.OrderBy(=>Guid.NewGuid().GetHashCode())
.第一次());
}
//改变状态的模拟结束
public static readonly dependencProperty StateProperty=dependencProperty.Register(“State”、typeof(EConState)、typeof(ConnectionVM),
新属性元数据(EConState.Disabled,(DependencyObject d,DependencyPropertyChangedEventArgs e)=>
{
if(d是ConnectionVM)
{
vm.ConName=$“{vm.Connection.Name}[{(EConState)e.NewValue}]”;
vm.PropertyChanged?.Invoke(vm,newpropertychangedeventargs(nameof(vm.State));
}
}));
//连接状态:影响是否显示连接并用于排序
公共经济国家
{
获取{return(EConState)GetValue(StateProperty);}
set{SetValue(StateProperty,value);}
}
//由模型basename和state创建的名称-通过StateProperty的回调进行更改
受保护的静态只读DependencyPropertyKey ConNamePropertyKey=DependencyProperty.RegisterReadOnly(“ConName”、typeof(string)、typeof(ConnectionVM)、new PropertyMetadata(“”);
public static readonly dependencProperty ConNameProperty=ConNamePropertyKey.dependencProperty;
公共字符串连接名
{
获取{return(string)GetValue(ConNameProperty);}
受保护集{SetValue(ConNamePropertyKey,value);}
}
连接{get;}
公共事件属性更改事件处理程序属性更改;
/// 
/// 
/// 
///连接-用于名称和初始状态
///定时器的延迟,直到状态改变开始
///状态变化之间的延迟
公共连接VM(连接连接、时间跨度延迟、时间跨度周期)
{
t=新计时器(changeMe,this,(int)delay.total毫秒,(int)period.total毫秒);
连接=连接;
State=Connection.State;//正在更改,由VM内的计时器模拟
}
}
公共类主视图模型
{
//RL中的所有连接:用户偶尔会添加新的连接
公共可观测集合Cons{get;set;}
//Cons上的筛选和排序视图-集合
公共ICollectionView ConsView{get;set;}
公共主视图模型(CollectionContainer cc)
{
//解调数据-通常由用户交互创建连接
//这模拟了每4s到10s改变一次状态的9个连接
Cons=新的可观测集合(
可枚举范围(1,9)
.Select(n=>newconnectionvm(newconnection($“Connection”{n}))
,TimeSpan.from毫秒(300*n)
,TimeSpan.from毫秒(700*(n+5
<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>
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);
    }

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