我正在尝试使用WPF创建一个界面,该界面可以一次显示和修改多个所选对象的属性。我知道这一定是可能的(Visual Studio中的属性网格可以做到),但我无法找到有关如何实现它的任何信息或示例。我已经在MultiBinding上找到了很多信息,但是规范的用例似乎是将一个UI字段绑定到同一个对象上的多个属性,而我正在尝试相反 - 将UI字段绑定到同一个属性在多个对象上。
更明确一点,我想要创建的行为是:
举例来说,这是我的一个旧的WinForms形式,它做同样的事情,我或多或少尝试在WPF中重新创建。在那种情况下,我在没有数据绑定的代码隐藏中处理它,这是一种我并不特别渴望重复的经验。
选择了一个项目:
选择了多个项目(元素类型,材质和Beta角度属性相同,其他属性不同):
我的特定用例的其他一些注意事项:
我目前关于如何执行此操作的最佳猜测是使用MultiBinding(或其自定义子类),跟踪底层集合中的更改,并以编程方式添加或删除每个对象上的属性绑定到MultiBinding Bindings集合,然后编写一个IMultiValueConverter来确定显示值。然而,这似乎是一个小提琴,不是真正的MultiBindings设计和互联网意见似乎不喜欢使用MultiBindings除了绝对必要的地方(虽然我不完全确定为什么)。有没有更好/更直接/标准的方式来做到这一点?
答案 0 :(得分:2)
在我看来,对象封装在这里真的会对你有所帮助,而不是试图让MultiBinding做一些它没有真正能够处理的东西。
所以,如果没有看到你的代码,我会做出几个假设:
ViewModel
代表每个对象。我们称之为ObjectViewModel
。ViewModel
。我们称之为PageViewModel
。 ObjectViewModel
可能具有以下属性:
string Name { get; set; }
string ElementType { get; set; }
string SelectionProfile { get; set; }
string Material { get; set; }
... etc
和PageViewModel
可能包含以下内容:
// Represents a list of selected items
ObjectSelectionViewModel SelectedItems { get; }
请注意新类ObjectSelectionViewModel
,它不仅代表您选择的项目,还允许您绑定它,就像它是单个对象一样。它可能看起来像这样:
public class ObjectSelectionViewModel : ObjectViewModel
{
// The current list of selected items.
public ObservableCollection<ObjectViewModel> SelectedItems { get; }
public ObjectSelectionViewModel()
{
SelectedItems = new ObservableCollection<ObjectViewModel>();
SelectedItems.CollectionChanged += (o, e) =>
{
// Pseudo-code here
if (items were added)
{
// Subscribe each to PropertyChanged, using Item_PropertyChanged
}
if (items were removed)
{
// Unsubscribe each from PropertyChanged
}
};
}
void Item_PropertyChanged(object sender, NotifyPropertyChangedArgs e)
{
// Notify that the local, group property (may have) changed.
NotifyPropertyChanged(e.PropertyName);
}
public override string Name
{
get
{
if (SelectedItems.Count == 0)
{
return "[None]";
}
if (SelectedItems.IsSameValue(i => i.Name))
{
return SelectedItems[0].Name;
}
return string.Empty;
}
set
{
if (SelectedItems.Count == 1)
{
SelectedItems[0].Name = value;
}
// NotifyPropertyChanged for the traditional MVVM ViewModel pattern.
NotifyPropertyChanged("Name");
}
}
public override string SelectionProfile
{
get
{
if (SelectedItems.Count == 0)
{
return "[None]";
}
if (SelectedItems.IsSameValue(i => i.SelectionProfile))
{
return SelectedItems[0].SelectionProfile;
}
return "[Multi]";
}
set
{
foreach (var item in SelectedItems)
{
item.SelectionProfile = value;
}
// NotifyPropertyChanged for the traditional MVVM ViewModel pattern.
NotifyPropertyChanged("SelectionProfile");
}
}
... etc ...
}
// Extension method for IEnumerable
public static bool IsSameValue<T, U>(this IEnumerable<T> list, Func<T, U> selector)
{
return list.Select(selector).Distinct().Count() == 1;
}
您甚至可以在此类上实现IList<ObjectViewModel>
和INotifyCollectionChanged
,将其转换为可直接绑定的功能齐全的列表。
答案 1 :(得分:2)
此功能不适用于WPF,但有一些选项可以实现此目的:
使用一些第三方控件,支持一次编辑多个对象,例如PropertyGrid from Extended WPF Toolkit
创建与对象具有相同属性但包装对象集合的包装器对象。然后绑定到这个包装类。
public class YourClassMultiEditWrapper{
private ICollection<YourClass> _objectsToEdit;
public YourClassMultiEditWrapper(ICollection<YourClass> objectsToEdit)
_objectsToEdit = objectsToEdit;
public string SomeProperty {
get { return _objectsToEdit[0].SomeProperty ; }
set { foreach(var item in _objectsToEdit) item.SomeProperty = value; }
}
}
public class YourClass {
public property SomeProperty {get; set;}
}
优点是它很简单。缺点是您需要为要编辑的每个类创建包装器。
3.您可以使用自定义TypeDescriptor
来创建通用包装类。在自定义TypeDescriptor
重写GetProperties()方法中,它将返回与对象相同的属性。您还需要使用已覆盖的PropertyDescriptor
和GetValue
方法创建自定义SetValue
,以便与您的对象集合进行编辑
public class MultiEditWrapper<TItem> : CustomTypeDescriptor {
private ICollection<TItem> _objectsToEdit;
private MultiEditPropertyDescriptor[] _propertyDescriptors;
public MultiEditWrapper(ICollection<TItem> objectsToEdit) {
_objectsToEdit = objectsToEdit;
_propertyDescriptors = TypeDescriptor.GetProperties(typeof(TItem))
.Select(p => new MultiEditPropertyDescriptor(objectsToEdit, p))
.ToArray();
}
public override PropertyDescriptorCollection GetProperties()
{
return new PropertyDescriptorCollection(_propertyDescriptors);
}
}
答案 2 :(得分:1)
这样的东西应该有效(在ViewModel中):
ObservableCollection<Item> _selectedItems;
// used to handle multi selection, the easiest is to set it from View in SelectionChanged event
public ObservableCollection<Item> SelectedItems
{
get { return _selectedItems; }
set
{
_selectedItems = value;
OnPropertyChanged();
// this will trigger View updating value from getter
OnPropertyChanged(nameof(SomeProperty));
}
}
// this will be one of your properties to edit, you'll have to do this for each property you want to edit
public double SomeProperty
{
get { return SelectedItems.Average(); } // as example
set
{
foreach(var item in SelectedItems)
item.SomeProperty = value;
}
}
然后将SomeProperty
绑定到必须显示/编辑其值的任何内容,您就完成了。
答案 3 :(得分:1)
我不认为你可以按照你想要的方式使绑定工作。但是,您可以通过在类型的包装类中处理它来使PropertyChanged事件对您有利。在下面的代码中,MultiEditable类处理EditItem属性的PropertyChanged事件。如果您有一个用户正在编辑梁的属性的表单,您需要将表单上的输入控件绑定到EditItem的属性。您将需要覆盖_EditItem_PropertyChanged,如图所示,从那里您可以更新所选项目的属性,因为EditItem的属性已更改。不要忘记取消处理事件。
编辑:我忘了添加代码来检查所有属性是否与某个值相同。这很容易做 - 只需检查集合并比较所有项目的属性与EditItem的相同属性。如果它们都是相同的返回true,否则&#34; Multi&#34;或者你需要什么。您还可以在代码中引用MultiEditable - 只需更新EditItem属性,所有项目和视觉效果都将更新。
public interface ISelectable
{
bool IsSelected { get; set; }
}
public abstract class MultiEditable<T> : ObservableCollection<T> where T:class,ISelectable,INotifyPropertyChanged
{
private T _EditItem;
public T EditItem
{
get { return _EditItem; }
set
{
if(_EditItem != value)
{
_EditItem = value;
_EditItem.PropertyChanged += _EditItem_PropertyChanged;
}
}
}
public bool AreMultipleItemsSelected
{
get { return this.Count(x => x.IsSelected) > 1; }
}
public virtual void _EditItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
}
public class MultiEditableBeams : MultiEditable<Beam>
{
public override void _EditItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
base._EditItem_PropertyChanged(sender, e);
foreach (Beam beam in this.Where(x => x.IsSelected))
{
if (e.PropertyName == "Material")
beam.Material = EditItem.Material;
else if (e.PropertyName == "Length")
beam.Length = EditItem.Length;
}
}
}
public class Beam : ISelectable, INotifyPropertyChanged
{
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set
{
if (_IsSelected != value)
{
_IsSelected = value;
RaisePropertyChanged();
}
}
}
private string _Material;
public string Material
{
get { return _Material; }
set
{
if (_Material != value)
{
Material = value;
RaisePropertyChanged();
}
}
}
private int _Length;
public int Length
{
get { return _Length; }
set
{
if (_Length != value)
{
_Length = value;
RaisePropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}