How, do I display a ObservableCollection<> of custom objects in the Xceed WPF PropertyGrid in which each List Item can be expanded to display the custom objects properties. (ie:
----PropertyGrid-----
CoreClass
(+/-) ObservableCollection< CustomClass >
(+/-) CustomClass.Object1
Property1: Value
Property2: Value
…
PropertyN: Value
(+/-) CustomClass.Object2
Property1: Value
Property2: Value
…
PropertyN: Value
If I use [ExpandableObject] on the ObservableCollection<> it only shows the Counts property.
Edit:(Added code)
MainWindow.xaml:
<Window x:Class="PropGridExample.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:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PropGridExample"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<xctk:PropertyGrid x:Name="PropertyGrid" SelectedObject="{Binding BindingItem}"></xctk:PropertyGrid>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
MainWindowViewModel mwvm = new MainWindowViewModel();
this.DataContext = mwvm;
InitializeComponent();
}
}
MainWindowViewModel.cs
public class MainWindowViewModel
{
public Item BindingItem { get; set; }
public MainWindowViewModel()
{
BindingItem = new Item();
}
public class Item
{
public int ID { get; set; }
[ExpandableObject()]
public ObservableCollection<CustomClass> Classes { get; set; }
public Item()
{
ID = 1;
Classes = new ObservableCollection<CustomClass>();
Classes.Add(new CustomClass() { Name = "CustomFoo" });
}
}
public class CustomClass
{
public string Name { get; set; }
[ExpandableObject()]
public ObservableCollection<type> Types { get; set; }
public CustomClass()
{
Types = new ObservableCollection<type>();
Types.Add(new type() { name = "foo", value = "bar" });
Types.Add(new type() { name = "bar", value = "foo" });
}
}
public class type
{
public string name { get; set; }
public string value { get; set; }
}
}
Note that most of this idea comes from the CodeProject project you linked to. The article gets you most of the way there, but as you note, it does not expand each item in the collection for the WPF PropertyGrid. In order to do that, each "item" needs to have an ExpandableObjectAttribute.
In order to allow future StackOverflow readers to understand, I'm going to start from beginning.
From the beginning
So, starting from this example:
public class MainWindowViewModel
{
/// <summary> This the object we want to be able to edit in the data grid. </summary>
public ComplexObject BindingComplexObject { get; set; }
public MainWindowViewModel()
{
BindingComplexObject = new ComplexObject();
}
}
public class ComplexObject
{
public int ID { get; set; }
public ObservableCollection<ComplexSubObject> Classes { get; set; }
public ComplexObject()
{
ID = 1;
Classes = new ObservableCollection<ComplexSubObject>();
Classes.Add(new ComplexSubObject() { Name = "CustomFoo" });
Classes.Add(new ComplexSubObject() { Name = "My Other Foo" });
}
}
public class ComplexSubObject
{
public string Name { get; set; }
public ObservableCollection<SimpleValues> Types { get; set; }
public ComplexSubObject()
{
Types = new ObservableCollection<SimpleValues>();
Types.Add(new SimpleValues() { name = "foo", value = "bar" });
Types.Add(new SimpleValues() { name = "bar", value = "foo" });
}
}
public class SimpleValues
{
public string name { get; set; }
public string value { get; set; }
}
In order for the WPF PropertyGrid to be able to edit each item in the ObservableCollection, we need to provide a type descriptor for the collection which return the items as "Properties" of that collection so they can be edited. Because we cannot statically determine the items from a collection (as each collection has different number of elements), it means that the collection itself must be the TypeDescriptor, which means implementing ICustomTypeDescriptor.
(note that only GetProperties is important for our purposes, the rest just delegates to TypeDescriptor):
public class ExpandableObservableCollection<T> : ObservableCollection<T>,
ICustomTypeDescriptor
{
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
// Create a collection object to hold property descriptors
PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
for (int i = 0; i < Count; i++)
{
pds.Add(new ItemPropertyDescriptor<T>(this, i));
}
return pds;
}
#region Use default TypeDescriptor stuff
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
return TypeDescriptor.GetAttributes(this, noCustomTypeDesc: true);
}
string ICustomTypeDescriptor.GetClassName()
{
return TypeDescriptor.GetClassName(this, noCustomTypeDesc: true);
}
string ICustomTypeDescriptor.GetComponentName()
{
return TypeDescriptor.GetComponentName(this, noCustomTypeDesc: true);
}
TypeConverter ICustomTypeDescriptor.GetConverter()
{
return TypeDescriptor.GetConverter(this, noCustomTypeDesc: true);
}
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(this, noCustomTypeDesc: true);
}
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(this, noCustomTypeDesc: true);
}
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(this, editorBaseType, noCustomTypeDesc: true);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
return TypeDescriptor.GetEvents(this, noCustomTypeDesc: true);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(this, attributes, noCustomTypeDesc: true);
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
return TypeDescriptor.GetProperties(this, attributes, noCustomTypeDesc: true);
}
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
#endregion
}
Additionally, we need an implementation of ItemPropertyDescriptor, which I provide here:
public class ItemPropertyDescriptor<T> : PropertyDescriptor
{
private readonly ObservableCollection<T> _owner;
private readonly int _index;
public ItemPropertyDescriptor(ObservableCollection<T> owner, int index)
: base("#" + index, null)
{
_owner = owner;
_index = index;
}
public override AttributeCollection Attributes
{
get
{
var attributes = TypeDescriptor.GetAttributes(GetValue(null), false);
if (!attributes.OfType<ExpandableObjectAttribute>().Any())
{
// copy all the attributes plus an extra one (the
// ExpandableObjectAttribute)
// this ensures that even if the type of the object itself doesn't have the
// ExpandableObjectAttribute, it will still be expandable.
var newAttributes = new Attribute[attributes.Count + 1];
attributes.CopyTo(newAttributes, newAttributes.Length - 1);
newAttributes[newAttributes.Length - 1] = new ExpandableObjectAttribute();
// overwrite the array
attributes = new AttributeCollection(newAttributes);
}
return attributes;
}
}
public override bool CanResetValue(object component)
{
return false;
}
public override object GetValue(object component)
{
return Value;
}
private T Value
=> _owner[_index];
public override void ResetValue(object component)
{
throw new NotImplementedException();
}
public override void SetValue(object component, object value)
{
_owner[_index] = (T)value;
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
public override Type ComponentType
=> _owner.GetType();
public override bool IsReadOnly
=> false;
public override Type PropertyType
=> Value?.GetType();
}
Which for the most part, just sets up reasonable defaults, which you can tweak to serve your needs.
One thing to note is that you may implement the Attributes property differently, depending on your use case. If you don't do the "add it to the attribute collection if it's not there", then you need to add the attribute to the classes/types that you want to expand; if you do keep that code in, then you'll be able to expand every item in the collection no matter if the class/type has the attribute or not.
It then becomes a matter of using ExpandableObservableCollection in place of ObservableCollection. This kind of sucks as it means your ViewModel has view-stuff-ish stuff in it, but ¯\_(ツ)_/¯.
Additionally, you need to add the ExpandableObjectAttribute to each of the properties that is a ExpandableObservableCollection.
Code Dump
If you're following along at home, you can use the following dialog code to run the example:
<Window x:Class="WpfDemo.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:WpfDemo"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<xctk:PropertyGrid x:Name="It" />
</Grid>
</Window>
-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace WpfDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
It.SelectedObject = new MainWindowViewModel().BindingComplexObject;
}
}
}
And here's the complete ViewModel implementation:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
namespace WpfDemo
{
public class MainWindowViewModel
{
/// <summary> This the object we want to be able to edit in the data grid. </summary>
public ComplexObject BindingComplexObject { get; set; }
public MainWindowViewModel()
{
BindingComplexObject = new ComplexObject();
}
}
[ExpandableObject]
public class ComplexObject
{
public int ID { get; set; }
[ExpandableObject]
public ExpandableObservableCollection<ComplexSubObject> Classes { get; set; }
public ComplexObject()
{
ID = 1;
Classes = new ExpandableObservableCollection<ComplexSubObject>();
Classes.Add(new ComplexSubObject() { Name = "CustomFoo" });
Classes.Add(new ComplexSubObject() { Name = "My Other Foo" });
}
}
[ExpandableObject]
public class ComplexSubObject
{
public string Name { get; set; }
[ExpandableObject]
public ExpandableObservableCollection<SimpleValues> Types { get; set; }
public ComplexSubObject()
{
Types = new ExpandableObservableCollection<SimpleValues>();
Types.Add(new SimpleValues() { name = "foo", value = "bar" });
Types.Add(new SimpleValues() { name = "bar", value = "foo" });
}
}
public class SimpleValues
{
public string name { get; set; }
public string value { get; set; }
}
public class ExpandableObservableCollection<T> : ObservableCollection<T>,
ICustomTypeDescriptor
{
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
// Create a collection object to hold property descriptors
PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
for (int i = 0; i < Count; i++)
{
pds.Add(new ItemPropertyDescriptor<T>(this, i));
}
return pds;
}
#region Use default TypeDescriptor stuff
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
return TypeDescriptor.GetAttributes(this, noCustomTypeDesc: true);
}
string ICustomTypeDescriptor.GetClassName()
{
return TypeDescriptor.GetClassName(this, noCustomTypeDesc: true);
}
string ICustomTypeDescriptor.GetComponentName()
{
return TypeDescriptor.GetComponentName(this, noCustomTypeDesc: true);
}
TypeConverter ICustomTypeDescriptor.GetConverter()
{
return TypeDescriptor.GetConverter(this, noCustomTypeDesc: true);
}
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(this, noCustomTypeDesc: true);
}
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(this, noCustomTypeDesc: true);
}
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(this, editorBaseType, noCustomTypeDesc: true);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
return TypeDescriptor.GetEvents(this, noCustomTypeDesc: true);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(this, attributes, noCustomTypeDesc: true);
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
return TypeDescriptor.GetProperties(this, attributes, noCustomTypeDesc: true);
}
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
#endregion
}
public class ItemPropertyDescriptor<T> : PropertyDescriptor
{
private readonly ObservableCollection<T> _owner;
private readonly int _index;
public ItemPropertyDescriptor(ObservableCollection<T> owner, int index)
: base("#" + index, null)
{
_owner = owner;
_index = index;
}
public override AttributeCollection Attributes
{
get
{
var attributes = TypeDescriptor.GetAttributes(GetValue(null), false);
if (!attributes.OfType<ExpandableObjectAttribute>().Any())
{
// copy all the attributes plus an extra one (the
// ExpandableObjectAttribute)
// this ensures that even if the type of the object itself doesn't have the
// ExpandableObjectAttribute, it will still be expandable.
var newAttributes = new Attribute[attributes.Count + 1];
attributes.CopyTo(newAttributes, newAttributes.Length - 1);
newAttributes[newAttributes.Length - 1] = new ExpandableObjectAttribute();
// overwrite the original
attributes = new AttributeCollection(newAttributes);
}
return attributes;
}
}
public override bool CanResetValue(object component)
{
return false;
}
public override object GetValue(object component)
{
return Value;
}
private T Value
=> _owner[_index];
public override void ResetValue(object component)
{
throw new NotImplementedException();
}
public override void SetValue(object component, object value)
{
_owner[_index] = (T)value;
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
public override Type ComponentType
=> _owner.GetType();
public override bool IsReadOnly
=> false;
public override Type PropertyType
=> Value?.GetType();
}
}
MackieChan provided the major clues for this...
There is no need to inherit from ICustomTypeDescriptor as similar results can be achieved using type converters.
Firstly create an expandable object type converter and override the GetProperties method. For example, if you wish to maintain the index order of a generic IList type:
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using System.ComponentModel;
public class MyExpandableIListConverter<T> : ExpandableObjectConverter
{
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
if (value is IList<T> list)
{
PropertyDescriptorCollection propDescriptions = new PropertyDescriptorCollection(null);
IEnumerator enumerator = list.GetEnumerator();
int counter = -1;
while (enumerator.MoveNext())
{
counter++;
propDescriptions.Add(new ListItemPropertyDescriptor<T>(list, counter));
}
return propDescriptions;
}
else
{
return base.GetProperties(context, value, attributes);
}
}
}
With the ListItemPropertyDescriptor being defined as follows:
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using System.ComponentModel;
public class ListItemPropertyDescriptor<T> : PropertyDescriptor
{
private readonly IList<T> owner;
private readonly int index;
public ListItemPropertyDescriptor(IList<T> owner, int index) : base($"[{index}]", null)
{
this.owner = owner;
this.index = index;
}
public override AttributeCollection Attributes
{
get
{
var attributes = TypeDescriptor.GetAttributes(GetValue(null), false);
//If the Xceed expandable object attribute is not applied then apply it
if (!attributes.OfType<ExpandableObjectAttribute>().Any())
{
attributes = AddAttribute(new ExpandableObjectAttribute(), attributes);
}
//set the xceed order attribute
attributes = AddAttribute(new PropertyOrderAttribute(index), attributes);
return attributes;
}
}
private AttributeCollection AddAttribute(Attribute newAttribute, AttributeCollection oldAttributes)
{
Attribute[] newAttributes = new Attribute[oldAttributes.Count + 1];
oldAttributes.CopyTo(newAttributes, 1);
newAttributes[0] = newAttribute;
return new AttributeCollection(newAttributes);
}
public override bool CanResetValue(object component)
{
return false;
}
public override object GetValue(object component)
{
return Value;
}
private T Value
=> owner[index];
public override void ResetValue(object component)
{
throw new NotImplementedException();
}
public override void SetValue(object component, object value)
{
owner[index] = (T)value;
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
public override Type ComponentType
=> owner.GetType();
public override bool IsReadOnly
=> false;
public override Type PropertyType
=> Value?.GetType();
}
Then you need to dynamically decorate the types you wish to display in the property grid with ExpandableObjectAttribute and TypeConverterAttribute. I create a 'decoration manager' to achieve this as follows.
using System.ComponentModel;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
public static class TypeDecorationManager
{
public static void AddExpandableObjectConverter(Type T)
{
TypeDescriptor.AddAttributes(T, new TypeConverterAttribute(typeof(ExpandableObjectConverter)));
TypeDescriptor.AddAttributes(T, new ExpandableObjectAttribute());
}
public static void AddExpandableIListConverter<I>(Type T)
{
TypeDescriptor.AddAttributes(T, new TypeConverterAttribute(typeof(MyExpandableIListConverter<I>)));
TypeDescriptor.AddAttributes(T, new ExpandableObjectAttribute());
}
}
Call AddExpandableObjectConverter for any type you would like to be expandable in the property grid and AddExpandableIListConverter for any IList type you would like to be expandable on the grid.
For example, if you have a curve object with some properties including an IList then all of the properties and list items can be made expandable as follows:
TypeDecorationManager.AddExpandableObjectConverter(typeof(Curve));
TypeDecorationManager.AddExpandableObjectConverter(typeof(CurvePoint));
AddCoreExpandableListConverter<CurvePoint>(typeof(IList<CurvePoint>));
Related
By default, PropertyGrid only allowed editing properties with public setter. I'd like to allow editing of properties without a setter.
For example:
class A {
public int X {get;set}
public int Y {get;}
}
In the example above, only X will be editable. Y will be displayed but grayed out. How can I make Y editable?
Note: making a private backing field would be OK. For example:
class A {
public int X {get;set}
private int y;
public int Y {get => y; }
}
You can build a wrapper/proxy class based on the ICustomTypeDescriptor Interface that allows you to tweak properties at runtime.
This is how you could use it:
var a = new A();
// build a proxy
var proxy = new Proxy(a);
// tweak any properties
proxy.Properties["Y"].IsReadOnly = false;
// you can also tweak attributes
proxy.Properties["Y"].Attributes.Add(new CategoryAttribute("R/O -> R/W"));
proxy.Properties["Y"].Attributes.Add(new DescriptionAttribute("This works"));
// handle property change
propertyGrid1.PropertyValueChanged += (s, e) =>
{
if (e.ChangedItem.PropertyDescriptor.Name == "Y")
{
a.Y = (int)e.ChangedItem.Value;
}
};
// select the proxy instead of the original instance
propertyGrid1.SelectedObject = proxy;
And here is the result
...
class A
{
public int X { get; set; }
public int Y { get; internal set; }
}
...
public class Proxy : ICustomTypeDescriptor
{
public Proxy(object instance)
{
if (instance == null)
throw new ArgumentNullException(nameof(instance));
Instance = instance;
Properties = TypeDescriptor.GetProperties(instance).OfType<PropertyDescriptor>().Select(d => new ProxyProperty(instance, d)).ToDictionary(p => p.Name);
}
public object Instance { get; }
public IDictionary<string, ProxyProperty> Properties { get; }
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(Instance);
public string GetClassName() => TypeDescriptor.GetClassName(Instance);
public string GetComponentName() => TypeDescriptor.GetComponentName(Instance);
public TypeConverter GetConverter() => TypeDescriptor.GetConverter(Instance);
public EventDescriptor GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(Instance);
public object GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(Instance, editorBaseType);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(Instance);
public EventDescriptorCollection GetEvents(Attribute[] attributes) => TypeDescriptor.GetEvents(Instance, attributes);
public PropertyDescriptor GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(Instance);
public PropertyDescriptorCollection GetProperties() => new PropertyDescriptorCollection(Properties.Values.Select(p => new Desc(this, p)).ToArray());
public PropertyDescriptorCollection GetProperties(Attribute[] attributes) => GetProperties();
public object GetPropertyOwner(PropertyDescriptor pd) => Instance;
private class Desc : PropertyDescriptor
{
public Desc(Proxy proxy, ProxyProperty property)
: base(property.Name, property.Attributes.ToArray())
{
Proxy = proxy;
Property = property;
}
public Proxy Proxy { get; }
public ProxyProperty Property { get; }
public override Type ComponentType => Proxy.GetType();
public override Type PropertyType => Property.PropertyType ?? typeof(object);
public override bool IsReadOnly => Property.IsReadOnly;
public override bool CanResetValue(object component) => Property.HasDefaultValue;
public override object GetValue(object component) => Property.Value;
public override void ResetValue(object component) { if (Property.HasDefaultValue) Property.Value = Property.DefaultValue; }
public override void SetValue(object component, object value) => Property.Value = value;
public override bool ShouldSerializeValue(object component) => Property.ShouldSerializeValue;
}
}
public class ProxyProperty
{
public ProxyProperty(string name, object value)
{
if (name == null)
throw new ArgumentNullException(nameof(value));
Name = name;
Value = value;
Attributes = new List<Attribute>();
}
public ProxyProperty(object instance, PropertyDescriptor descriptor)
{
if (descriptor == null)
throw new ArgumentNullException(nameof(descriptor));
Name = descriptor.Name;
Value = descriptor.GetValue(instance);
var def = descriptor.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault();
if (def != null)
{
HasDefaultValue = true;
DefaultValue = def.Value;
}
IsReadOnly = (descriptor.Attributes.OfType<ReadOnlyAttribute>().FirstOrDefault()?.IsReadOnly).GetValueOrDefault();
ShouldSerializeValue = descriptor.ShouldSerializeValue(instance);
Attributes = descriptor.Attributes.Cast<Attribute>().ToList();
PropertyType = descriptor.PropertyType;
}
public string Name { get; }
public object Value { get; set; }
public object DefaultValue { get; set; }
public bool HasDefaultValue { get; set; }
public bool IsReadOnly { get; set; }
public bool ShouldSerializeValue { get; set; }
public Type PropertyType { get; set; }
public IList<Attribute> Attributes { get; }
}
There is a way to do this, but it is absurdly complex;
you create a custom TypeDescriptionProvider and link it to the type, or implement ICustomTypeDescriptor in the type, and
you create a custom PropertyDescriptor that knows how to edit the field in the GetValue and SetValue
you create a custom TypeDescriptor that echos the standard property descriptors for most things, and your new property descriptor in this case
In all seriousness, though; please don't do this! Just make the property accessible in the first place. If it isn't a settable property, you shouldn't be trying to set it.
based on the comments it sounds like what you actually need is "popsicle immutability"; consider:
class Foo {
private bool _frozen;
public void Freeze() => _frozen = true;
protected void ThrowIfFrozen() {
if (_frozen) throw new InvalidOperationException(
"The object cannot be changed once Freeze has been called");
}
private int _x, _y;
public int X {
get => _x;
set {
if (value != _x) {
ThrowIfFrozen();
_x = value;
}
}
}
public int Y {
get => _y;
set {
if (value != _y) {
ThrowIfFrozen();
_y = value;
}
}
}
}
I searched this, but I didn't find the answer to this specific question (although there are many similar ones).
I have a Column class in my ViewModel like this:
public class Column
{
public string Header { get; set; }
public ObservableCollection<double> Data { get; private set; }
public DummyColumn()
{
Data = new ObservableCollection<double>();
}
}
In MyViewModel class itself, I have Columns property:
public class MyViewModel
{
public ObservableCollection<Column> { get; private set; }
public MyViewModel()
{
Columns = new ObservableCollection<DummyColumn>();
var c1 = new DummyColumn() { Header = "A" };
c1.Data.Add(5);
c1.Data.Add(6);
Columns.Add(c1);
var c2 = new DummyColumn() { Header = "B" };
c2.Data.Add(51);
c2.Data.Add(61);
Columns.Add(c2);
}
}
I want to bind the Columns property of the latter class to the columns of a DataGrid in view. For each Column instance, I want to show the Header property as the column's header and its Data property as cells' values. How can I do that?
I reached this goal using ideas from here and here. I implemented three classes: Table, Row and RowPropertyDescriptor in my VM. Here is the code:
class RowPropertyDescriptor : PropertyDescriptor
{
private int index;
public RowPropertyDescriptor(string name, int index)
: base(name, null)
{
this.index = index;
}
#region PropertyDescriptor
public override string DisplayName { get { return Name; } }
public override Type ComponentType { get { return typeof(double); } }
public override bool IsReadOnly { get { return false; } }
public override Type PropertyType { get { return typeof(double); } }
public override object GetValue(object component)
{
return ((Row)component)[index];
}
public override void SetValue(object component, object value)
{
((Row)component)[index] = (double)value;
}
public override bool CanResetValue(object component)
{
return false;
}
public override void ResetValue(object component)
{
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
#endregion
}
class Row : DynamicObject
{
private Table table;
private int row;
public Row(Table namedArraysView, int row)
{
this.table = namedArraysView;
this.row = row;
}
public double this[int col]
{
get { return table.RawData[col].Data[row]; }
set { table.RawData[col].Data[row] = value; }
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
int idx;
bool found = table.PropertiesIndex.TryGetValue(binder.Name, out idx);
if (found)
{
try
{
this[idx] = Convert.ToDouble(value);
return true;
}
catch (Exception ex)
{
return false;
}
}
return base.TrySetMember(binder, value);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
int idx;
bool found = table.PropertiesIndex.TryGetValue(binder.Name, out idx);
if (found)
{
result = this[idx];
return true;
}
return base.TryGetMember(binder, out result);
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return table.PropertyNames;
}
}
class Table : BindingList<Row>, ITypedList
{
public ObservableCollection<INamedArray> RawData { get; private set; }
internal List<string> PropertyNames { get; private set; }
internal Dictionary<string, int> PropertiesIndex { get; private set; }
public Table(ObservableCollection<INamedArray> headeredArrays)
{
bind(headeredArrays);
headeredArrays.CollectionChanged += (object sender, NotifyCollectionChangedEventArgs e) => { bind(headeredArrays); };
}
private void bind(ObservableCollection<INamedArray> headeredArrays)
{
Clear();
if (headeredArrays == null)
{
RawData = null;
PropertyNames = null;
PropertiesIndex = null;
return;
}
RawData = headeredArrays;
PropertyNames = RawData.Select(d => d.Name).ToList();
PropertiesIndex = new Dictionary<string, int>();
for (int i = 0; i < RawData.Count; i++)
PropertiesIndex.Add(RawData[i].Name, i);
int nRows = headeredArrays[0].Data.Count;
for (int i = 0; i < nRows; i++)
Add(new Row(this, i));
}
#region ITypedList
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
var dynamicDescriptors = new List<PropertyDescriptor>();
if (this[0].GetDynamicMemberNames() == null) return new PropertyDescriptorCollection(new PropertyDescriptor[] { });
var memberNames = this[0].GetDynamicMemberNames().ToArray();
for (int i = 0; i < memberNames.Length; i++)
dynamicDescriptors.Add(new RowPropertyDescriptor(memberNames[i], i));
return new PropertyDescriptorCollection(dynamicDescriptors.ToArray());
}
public string GetListName(PropertyDescriptor[] listAccessors)
{
return null;
}
#endregion
}
Then, one can easily create a Table with passing his/her columns to this class. This Table can be visualised correctly in view. The only limitation is that binding is one way which is not very hard to work around.
I want people who use my custom control to choose a constant with a simple dropdown. The control should be reusable and therefor resides in a library so it can't access an enum, and the constants could in fact be defined anywhere.
Problem is that AppDomain.CurrentDomain.GetAssemblies() does not contain the current project you are designing in. And when I create a separate project it sometimes works but mostly it doesn't. And with doesn't work I mean: It can't find any classes/constants defined in the library, so they don't show up in the dropdown.
Everything else already works. The dropdown works. Code generation is a but clunky but it works. And I can always find constants defined in TestControl itself.
It seems like the designer itself doesn't always load all the assemblies/projects which are available. So how can I load the project which I need within the context of the designer/visual studio itself? Or how do I reference/find other projects within my list of dependencies from within the UITypeEditor?
I have created my own TestControl:
public partial class TestControl : UserControl, IHasTags
{
public static readonly ITestItem A = new TestItemImpl(null, "A", "Aap");
public static readonly ITestItem B = new TestItemImpl(null, "B", "B");
public static readonly ITestItem C_xx = new TestItemImpl(null, "C", "C");
public static readonly ITestItem D = new TestItemImpl(null, "D", "D");
[Editor(typeof(ItemEditor), typeof(UITypeEditor))]
public ITestItem item { get; set; }
public TestControl()
{
InitializeComponent();
}
}
With my own editor:
public class ItemEditor : UITypeEditor
{
private IWindowsFormsEditorService _editorService;
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
// drop down mode (we'll host a listbox in the drop down)
return UITypeEditorEditStyle.DropDown;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
_editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
// use a list box
ListBox lb = new ListBox();
lb.SelectionMode = SelectionMode.One;
lb.SelectedValueChanged += OnListBoxSelectedValueChanged;
lb.Width = 300;
// get the analytic object from context
// this is how we get the list of possible benchmarks
TestControl ctrl = (TestControl)context.Instance;
// Add all test items
IList<StaticFieldInfo<ITestItem>> items = ClassHelper.getStaticFields<IHasTags, ITestItem>();
foreach (StaticFieldInfo<ITestItem> item in items)
{
lb.Items.Add(item);
}
// show this model stuff
_editorService.DropDownControl(lb);
if (lb.SelectedItem == null) // no selection, return the passed-in value as is
return value;
StaticFieldInfo<ITestItem> result = (StaticFieldInfo<ITestItem>)lb.SelectedItem;
return result.value;
}
private void OnListBoxSelectedValueChanged(object sender, EventArgs e)
{
// close the drop down as soon as something is clicked
_editorService.CloseDropDown();
}
}
And I collect all constants with the following helper:
public class StaticFieldInfo<T>
{
public StaticFieldInfo(FieldInfo fieldInfo, T value)
{
this.fieldInfo = fieldInfo;
this.value = value;
}
public T value { get; private set; }
public FieldInfo fieldInfo { get; private set; }
public override string ToString()
{
return value.ToString() + " (" + fieldInfo.DeclaringType.FullName + "." + fieldInfo.Name + ")";
}
}
public class ClassHelper
{
public static IList<StaticFieldInfo<T>> getStaticFields<C, T>()
{
Type classType = typeof(C);
Type fieldType = typeof(T);
List<StaticFieldInfo<T>> result = new List<StaticFieldInfo<T>>();
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
foreach (Type t in asm.GetTypes())
{
if (classType.IsAssignableFrom(t))
{
foreach (FieldInfo f in t.GetFields())
{
if (f.IsStatic && fieldType.IsAssignableFrom(f.FieldType))
{
T value = (T)f.GetValue(null);
if (value != null)
{
result.Add(new StaticFieldInfo<T>(f, value));
}
}
}
}
}
}
catch (ReflectionTypeLoadException)
{
// Ignore errors
}
}
return result;
}
}
The rest of the code (less important):
public interface IHasTags
{
}
[TypeConverter(typeof(ITestItemConverter))]
public interface ITestItem
{
string driver { get; set; }
string tag { get; set; }
string name { get; set; }
}
public class ITestItemConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
// we only know how to convert from to a string
return typeof(string) == destinationType;
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value != null)
{
return value.ToString();
}
return "(none)";
}
}
[DesignerSerializer(typeof(ItemSerializer), typeof(CodeDomSerializer))]
public class TestItemImpl : ITestItem
{
public TestItemImpl()
{
}
public TestItemImpl(string driver, string tag, string name)
{
this.driver = driver;
this.tag = tag;
this.name = name;
}
public override string ToString()
{
return name;
}
public string driver { get; set; }
public string tag { get; set; }
public string name { get; set; }
}
public class ItemSerializer : CodeDomSerializer
{
public override object Deserialize(IDesignerSerializationManager manager, object codeObject)
{
// This is how we associate the component with the serializer.
CodeDomSerializer baseClassSerializer = (CodeDomSerializer)manager.
GetSerializer(typeof(TestItemImpl).BaseType, typeof(CodeDomSerializer));
/* This is the simplest case, in which the class just calls the base class
to do the work. */
return baseClassSerializer.Deserialize(manager, codeObject);
}
public override object Serialize(IDesignerSerializationManager manager, object value)
{
/* Associate the component with the serializer in the same manner as with
Deserialize */
CodeDomSerializer baseClassSerializer = (CodeDomSerializer)manager.
GetSerializer(typeof(TestItemImpl).BaseType, typeof(CodeDomSerializer));
object codeObject = baseClassSerializer.Serialize(manager, value);
/* Anything could be in the codeObject. This sample operates on a
CodeStatementCollection. */
if (codeObject is CodeStatementCollection)
{
CodeStatementCollection statements = (CodeStatementCollection)codeObject;
StaticFieldInfo<ITestItem> field = null;
foreach (StaticFieldInfo<ITestItem> item in ClassHelper.getStaticFields<IHasTags, ITestItem>())
{
if (value == item.value)
{
field = item;
break;
}
}
// If field can be found (should be always)
if (field != null)
{
statements = new CodeStatementCollection();
statements.Add(new CodeSnippetExpression(typeof(ITestItem).FullName + " " + this.GetUniqueName(manager, value) + " = " + typeof(TestControl).FullName + "." + field.fieldInfo.Name));
codeObject = statements;
}
}
return codeObject;
}
}
How can override the type of View property to my custom type.
My CustomGroupListCollectionView type adds extra property to the Groups property.
During runtime when i observe the type of View property is ListCollectionView, i want to change this to CustomGroupListCollectionView.
public class CollectionViewSourceCustom : CollectionViewSource
{
public new CustomGroupListCollectionView View { get; set; }
}
public class CustomGroupListCollectionView : ListCollectionView
{
private readonly CustomGroup _allGroup;
public CustomGroupListCollectionView(IList list)
: base(list)
{
_allGroup = new CustomGroup("All");
foreach (var item in list)
{
_allGroup.AddItem(item);
}
}
public override ReadOnlyObservableCollection<object> Groups
{
get
{
var group = new ObservableCollection<object>(base.Groups.ToList());
group.Add(_allGroup);
return new ReadOnlyObservableCollection<object>(group);
}
}
}
public class CustomGroup : CollectionViewGroup
{
public CustomGroup(object name)
: base(name)
{
}
public void AddItem(object item)
{
ProtectedItems.Add(item);
}
public override bool IsBottomLevel
{
get { return true; }
}
bool _IsChecked;
public bool IsChecked
{
get { return _IsChecked; }
set { _IsChecked = value; }
}
Visibility _CheckBoxVisibility;
public Visibility CheckBoxVisibility
{
get { return _CheckBoxVisibility; }
set { _CheckBoxVisibility = value; }
}
bool _IsExpanded;
public bool IsExpanded
{
get { return _IsExpanded; }
set { _IsExpanded = value; }
}
Visibility _ExpanderVisibility;
public Visibility ExpanderVisibility
{
get { return _ExpanderVisibility; }
set { _ExpanderVisibility = value; }
}
Visibility _ImageVisibility = Visibility.Collapsed;
public Visibility ImageVisibility
{
get { return _ImageVisibility; }
set { _ImageVisibility = value; }
}
}
CollectionViewSource has a CollectionViewType property, which you can use to determine the type of CollectionView the CollectionViewSource returns, like
<CollectionViewSource x:Key="source" CollectionViewType="{x:Type my:CustomGroupListCollectionView}" Source="{Binding MyData}"/>
As you see, you don't even have to create a new CollectionViewSource class.
If you still persist on using your way I would suggest this code:
public class CollectionViewSourceCustom : CollectionViewSource
{
public CollectionViewSourceCustom()
: base()
{
((ISupportInitialize)this).BeginInit();
this.CollectionViewType = typeof(CustomGroupListCollectionView);
((ISupportInitialize)this).EndInit();
}
}
Hope it helps.
Would like to display extracted data from xml file on property grid. XML file looks something like this:
<?xml version="1.0" encoding="utf-8" ?>
<customer>
<cust id="1">
<id>120</id>
<name>hello</name>
</cust>
<cust id="2">
<name>hello12</name>
<id>12012</id>
</cust>
</customer>
Now I want to extract data from XML on a property grid with server id as category(i.e it has to display cust id="1" in one category and cust id="2" in second category)
Try this code sample:
[TypeConverter(typeof(CustomerObjectConverter))]
public class Customer
{
internal readonly int ServerId;
public int Id { get; set; }
public string Name { get; set; }
public Customer(int serverId)
{
ServerId = serverId;
}
public override string ToString()
{
return Id + ", " + Name;
}
}
public class CustomerCollection : CollectionBase, ICustomTypeDescriptor
{
public CustomerCollection()
{
}
public CustomerCollection(IEnumerable<Customer> collection)
{
foreach (var item in collection)
Add(item);
}
public void Add(Customer emp)
{
List.Add(emp);
}
public void Remove(Customer emp)
{
List.Remove(emp);
}
public Customer this[int index]
{
get { return (Customer)List[index]; }
}
public String GetClassName()
{
return TypeDescriptor.GetClassName(this, true);
}
public AttributeCollection GetAttributes()
{
return TypeDescriptor.GetAttributes(this, true);
}
public String GetComponentName()
{
return TypeDescriptor.GetComponentName(this, true);
}
public TypeConverter GetConverter()
{
return TypeDescriptor.GetConverter(this, true);
}
public EventDescriptor GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(this, true);
}
public PropertyDescriptor GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(this, true);
}
public object GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(this, editorBaseType, true);
}
public EventDescriptorCollection GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(this, attributes, true);
}
public EventDescriptorCollection GetEvents()
{
return TypeDescriptor.GetEvents(this, true);
}
public object GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return GetProperties();
}
public PropertyDescriptorCollection GetProperties()
{
PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
for (int i = 0; i < List.Count; i++)
{
CustomersCollectionPropertyDescriptor pd = new CustomersCollectionPropertyDescriptor(this, i);
pds.Add(pd);
}
return pds;
}
}
internal class CustomersCollectionPropertyDescriptor : PropertyDescriptor
{
private readonly CustomerCollection collection;
private readonly int index = -1;
public CustomersCollectionPropertyDescriptor(CustomerCollection coll, int idx)
: base("#" + idx.ToString(), null)
{
collection = coll;
index = idx;
}
public override AttributeCollection Attributes
{
get { return new AttributeCollection(null); }
}
public override string Category
{
get { return "Server " + collection[index].ServerId; }
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get { return collection.GetType(); }
}
public override string DisplayName
{
get { return "Customer " + (index + 1); }
}
public override string Description
{
get { return "Customer"; }
}
public override object GetValue(object component)
{
return collection[index];
}
public override bool IsReadOnly
{
get { return true; }
}
public override string Name
{
get { return "#" + index.ToString(); }
}
public override Type PropertyType
{
get { return collection[index].GetType(); }
}
public override void ResetValue(object component) {}
public override bool ShouldSerializeValue(object component)
{
return false;
}
public override void SetValue(object component, object value)
{
}
}
internal class CustomerObjectConverter : ExpandableObjectConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return false;
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
return destinationType == typeof(string) && value is Customer
? value.ToString()
: base.ConvertTo(context, culture, value, destinationType);
}
}
Usage. Paste the following code into constructor of the form containing PropertyGrid with the name propertyGrid:
const string xmlString = "<customer><cust id=\"1\"><id>120</id><name>hello</name></cust><cust id=\"2\"><name>hello12</name><id>12012</id></cust></customer>";
XmlDocument xml = new XmlDocument();
xml.LoadXml(xmlString);
XmlNode root = xml["customer"];
CustomerCollection customers = new CustomerCollection();
foreach (XmlNode node in root.ChildNodes)
customers.Add(new Customer(int.Parse(node.Attributes["id"].Value))
{
Id = int.Parse(node["id"].InnerText),
Name = node["name"].InnerText
});
propertyGrid.SelectedObject = customers;