C# ObservableCollection(MCVE)的ICollectionView上的静态/动态混合上下文菜单
我正在使用XAML MenuItems(又名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 -
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>