I have a class MyClassA that has an IList property. I am using a PropertyGrid control to display all the properties of MyClassA and I would like the list of MyClassB to be displayed and editable via the PropertyGrid for MyClassA.
I currently have all the other properties being displayed in the Property grid except for the property that is the list of MyClassB. How do I go about adding the List of MyClassB to the property grid where the user can add/edit/remove items from the List?
I haven't really been able to find any examples that go into detail on this as of yet although I am still digging.
Here is a solution I have worked out so far, although it still doesn't fit in 100% to what I am looking for.
I found this reference to modify for my liking: http://www.codeproject.com/KB/tabs/customizingcollectiondata.aspx
What I did was create a new class that inherits from CollectionBase and that uses an ICustomTypeDescriptor.
After I did this and implemented the basic functionality, I had to create a PropertyDescriptor for the class.
Here is the code:
public class ZoneCollection : CollectionBase, ICustomTypeDescriptor
{
#region Collection Implementation
/// <summary>
/// Adds an zone object to the collection
/// </summary>
/// <param name="emp"></param>
public void Add(Zone zone)
{
this.List.Add(zone);
}
/// <summary>
/// Removes an zone object from the collection
/// </summary>
/// <param name="emp"></param>
public void Remove(Zone zone)
{
this.List.Remove(zone);
}
/// <summary>
/// Returns an zone object at index position.
/// </summary>
public Zone this[int index]
{
get
{
return (Zone)this.List[index];
}
}
#endregion
// Implementation of interface ICustomTypeDescriptor
#region ICustomTypeDescriptor impl
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;
}
/// <summary>
/// Called to get the properties of this type. Returns properties with certain
/// attributes. this restriction is not implemented here.
/// </summary>
/// <param name="attributes"></param>
/// <returns></returns>
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return GetProperties();
}
/// <summary>
/// Called to get the properties of this type.
/// </summary>
/// <returns></returns>
public PropertyDescriptorCollection GetProperties()
{
// Create a collection object to hold property descriptors
PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
// Iterate the list of employees
for (int i = 0; i < this.List.Count; i++)
{
// Create a property descriptor for the zone item and add to the property descriptor collection
ZoneCollectionPropertyDescriptor pd = new ZoneCollectionPropertyDescriptor(this, i);
pds.Add(pd);
}
// return the property descriptor collection
return pds;
}
#endregion
}
/// <summary>
/// Summary description for CollectionPropertyDescriptor.
/// </summary>
public class ZoneCollectionPropertyDescriptor : PropertyDescriptor
{
private ZoneCollection collection = null;
private int index = -1;
public ZoneCollectionPropertyDescriptor(ZoneCollection coll, int idx) :
base("#" + idx.ToString(), null)
{
this.collection = coll;
this.index = idx;
}
public override AttributeCollection Attributes
{
get
{
return new AttributeCollection(null);
}
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get
{
return this.collection.GetType();
}
}
public override string DisplayName
{
get
{
Zone zone = this.collection[index];
return zone.ID.ToString();
}
}
public override string Description
{
get
{
Zone zone = this.collection[index];
StringBuilder sb = new StringBuilder();
sb.Append(zone.ID.ToString());
if ( zone.Streets.Route != String.Empty || zone.Streets.Crossing != String.Empty)
sb.Append("::");
if (zone.Streets.Route != String.Empty)
sb.Append(zone.Streets.Route);
if ( zone.Streets.Crossing != String.Empty)
{
sb.Append(" and ");
sb.Append(zone.Streets.Crossing);
}
return sb.ToString();
}
}
public override object GetValue(object component)
{
return this.collection[index];
}
public override bool IsReadOnly
{
get { return false; }
}
public override string Name
{
get { return "#" + index.ToString(); }
}
public override Type PropertyType
{
get { return this.collection[index].GetType(); }
}
public override void ResetValue(object component)
{
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
public override void SetValue(object component, object value)
{
// this.collection[index] = value;
}
}
Intersection now contains a ZoneCollection instead of an IList and I can now edit/add/remove the zones contained within the collection.
Now, if I could make this more generic I'd be relatively happy. Another hindrance for my model is that I had to inherit from Collection base using this, instead of IList. This completely broke my mapping of my class for NHibernate and I'm now having to try and figure out how to remap this list using the method mentioned above.
If anyone wants to elaborate this any further I'd greatly appreciate some more insight.
I know this Topic is more than 2 years old, but maybe this could be interesting for you.
I had a similar Problem.
Starting with: I need a Point in 3D-Space which should be configurable in Property-Grid
For this I created a Class Koord. To make it changeable in PropertyGrid, I created a new Class "KoordConverter : TypeConverter"
This is used in a Vexel (check Wikipedia to find out what it's for :-) )
To create an TestBock (some 3D-Object) I'm using a List of Vexels.
Unfortunately I need a List of TestBlocks in my Program, Visible through the Property-Grid.
To start Topmost:
public partial class FormMain : Form
{
private BlockProperties _bp = new BlockProperties();
public FormMain()
{
InitializeComponent();
pgProperties.SelectedObject = _bp;
}
[...]
}
The Class BlockProperties includes the List of TestBocks which I filled a bit to show you what's inside.
class BlockProperties
{
public List<TestBocks> Testing { get; set; }
public BlockProperties()
{
Testing = new List<TestBocks>(3);
List<Vexel> t1 = new List<Vexel>(1);
t1.Add(new Vexel(new Koord(1,0,1), 1));
List<Vexel> t2 = new List<Vexel>(2);
t2.Add(new Vexel(new Koord(2, 0, 1), 2));
t2.Add(new Vexel(new Koord(2, 0, 2), 2));
List<Vexel> t3 = new List<Vexel>(3);
t3.Add(new Vexel(new Koord(3, 0, 1), 3));
t3.Add(new Vexel(new Koord(3, 0, 2), 3));
t3.Add(new Vexel(new Koord(3, 0, 3), 3));
TestBocks tb1 = new TestBocks();
tb1.Koords = t1;
TestBocks tb2 = new TestBocks();
tb2.Koords = t2;
TestBocks tb3 = new TestBocks();
tb3.Koords = t3;
Testing.Add(tb1);
Testing.Add(tb2);
Testing.Add(tb3);
[...]
}
[...]
}
Next is my TestBlock class, which is simply straight forward
[Serializable]
public class TestBocks
{
public List<Vexel> Vexels{ get; set; }
public TestBocks()
{
Vexels = new List<Vexel>();
}
}
In the Vexels is most of the magic I need for my Program:
I even put a ToString() here to make it easy during debugging.
public class Vexel
{
private Koord _origin;
private double _extent;
public Koord Origin { get { return _origin; } set { _origin = value; } }
public double Extent { get { return _extent; } set { _extent = value; } }
public string ToString()
{
NumberFormatInfo nFormatInfo = new NumberFormatInfo
{
NumberDecimalSeparator = ".",
NumberGroupSeparator = ""
};
return String.Format(nFormatInfo, "Origin;{0};{1};{2};Extent;{3}", _origin.X, _origin.Y, _origin.Z, _extent);
}
public Vexel()
{
_origin = new Koord(0,0,0);
Extent = 0;
}
public Vexel(Koord origin, double extent)
{
//TODO do some checking
_origin = origin;
_extent = extent;
}
So far everything worked fine for the PropertyGrid, but I could not edit the Koords.
The Class was pretty simple but not editable in the PropertyGrid.
Adding a TypeConverterClass solved this Problem (you can find the TypeConverter below the code of the Koord)
[TypeConverter(typeof(KoordConverter))]
[Serializable]
public class Koord
{
private double p_1;
private double p_2;
private double p_3;
public Koord(double x, double y, double z)
{
this.p_1 = x;
this.p_2 = y;
this.p_3 = z;
}
public string ToString()
{
return String.Format("X;{0};Y;{1};Z;{2}", p_1, p_2, p_3);
}
public double X { get { return p_1; } }
public double Y { get { return p_2; } }
public double Z { get { return p_3; } }
}
The Typeconverter was the most complicated code to write. You can find it below:
public class KoordConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string text = value as string;
if (text == null)
{
return base.ConvertFrom(context, culture, value);
}
string text2 = text.Trim();
if (text2.Length == 0)
{
return null;
}
if (culture == null)
{
culture = CultureInfo.CurrentCulture;
}
char c = culture.TextInfo.ListSeparator[0];
string[] array = text2.Split(new char[]
{
c
});
int[] array2 = new int[array.Length];
TypeConverter converter = TypeDescriptor.GetConverter(typeof(int));
for (int i = 0; i < array2.Length; i++)
{
array2[i] = (int)converter.ConvertFromString(context, culture, array[i]);
}
if (array2.Length == 3)
{
return new Koord(array2[0], array2[1], array2[2]);
}
throw new ArgumentException("TextParseFailedFormat");
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
{
throw new ArgumentNullException("destinationType");
}
if (value is Koord)
{
if (destinationType == typeof(string))
{
Koord Koord = (Koord)value;
if (culture == null)
{
culture = CultureInfo.CurrentCulture;
}
string separator = culture.TextInfo.ListSeparator + " ";
TypeConverter converter = TypeDescriptor.GetConverter(typeof(int));
string[] array = new string[3];
int num = 0;
array[num++] = converter.ConvertToString(context, culture, Koord.X);
array[num++] = converter.ConvertToString(context, culture, Koord.Y);
array[num++] = converter.ConvertToString(context, culture, Koord.Z);
return string.Join(separator, array);
}
if (destinationType == typeof(InstanceDescriptor))
{
Koord Koord2 = (Koord)value;
ConstructorInfo constructor = typeof(Koord).GetConstructor(new Type[]
{
typeof(double),
typeof(double),
typeof(double)
});
if (constructor != null)
{
return new InstanceDescriptor(constructor, new object[]
{
Koord2.X,
Koord2.Y,
Koord2.Z
});
}
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
{
if (propertyValues == null)
{
throw new ArgumentNullException("propertyValues");
}
object obj = propertyValues["X"];
object obj2 = propertyValues["Y"];
object obj3 = propertyValues["Z"];
if (obj == null || obj2 == null || obj3 == null || !(obj is double) || !(obj2 is double) || !(obj3 is double))
{
throw new ArgumentException("PropertyValueInvalidEntry");
}
return new Koord((double)obj, (double)obj2, (double)obj3);
}
public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(Koord), attributes);
return properties.Sort(new string[]
{
"X",
"Y",
"Z"
});
}
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
}
Basically after all of this was set up, It was no ploblem to modify any List of objects (TestBlocks or the Vexels within each TestBlock)
Hope it helps someone if they step over this Thread.
Best Regards
Robin Blood
PS:Editing is no problem in the PropertyGrid, maybe you just didn't get your constructors right !?
http://i.stack.imgur.com/LD3zf.png
Related
I have the following protoc3 message:
message LocalizedString {
map<string, string> translations = 1
}
When compiled into C#, I get the following autogenerated code:
using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace PKIo {
/// <summary>Holder for reflection information generated from io/common/localization.proto</summary>
public static partial class LocalizationReflection {
#region Descriptor
/// <summary>File descriptor for io/common/localization.proto</summary>
public static pbr::FileDescriptor Descriptor {
get { return descriptor; }
}
private static pbr::FileDescriptor descriptor;
static LocalizationReflection() {
byte[] descriptorData = global::System.Convert.FromBase64String(
string.Concat(
"Chxpby9jb21tb24vbG9jYWxpemF0aW9uLnByb3RvEgJpbyKDAQoPTG9jYWxp",
"emVkU3RyaW5nEjsKDHRyYW5zbGF0aW9ucxgBIAMoCzIlLmlvLkxvY2FsaXpl",
"ZFN0cmluZy5UcmFuc2xhdGlvbnNFbnRyeRozChFUcmFuc2xhdGlvbnNFbnRy",
"eRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQi1aJHN0YXNoLnBh",
"c3NraXQuY29tL2lvL21vZGVsL3Nkay9nby9pb6oCBFBLSW9iBnByb3RvMw=="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { },
new pbr::GeneratedClrTypeInfo(null, new pbr::GeneratedClrTypeInfo[] {
new pbr::GeneratedClrTypeInfo(typeof(global::PKIo.LocalizedString), global::PKIo.LocalizedString.Parser, new[]{ "Translations" }, null, null, new pbr::GeneratedClrTypeInfo[] { null, })
}));
}
#endregion
}
#region Messages
/// <summary>
/// Localized strings are optionally used to provide translated values for each of supported language.
/// </summary>
public sealed partial class LocalizedString : pb::IMessage<LocalizedString> {
private static readonly pb::MessageParser<LocalizedString> _parser = new pb::MessageParser<LocalizedString>(() => new LocalizedString());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public static pb::MessageParser<LocalizedString> Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public static pbr::MessageDescriptor Descriptor {
get { return global::PKIo.LocalizationReflection.Descriptor.MessageTypes[0]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
pbr::MessageDescriptor pb::IMessage.Descriptor {
get { return Descriptor; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public LocalizedString() {
OnConstruction();
}
partial void OnConstruction();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public LocalizedString(LocalizedString other) : this() {
translations_ = other.translations_.Clone();
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public LocalizedString Clone() {
return new LocalizedString(this);
}
/// <summary>Field number for the "translations" field.</summary>
public const int TranslationsFieldNumber = 1;
private static readonly pbc::MapField<string, string>.Codec _map_translations_codec
= new pbc::MapField<string, string>.Codec(pb::FieldCodec.ForString(10), pb::FieldCodec.ForString(18), 10);
private readonly pbc::MapField<string, string> translations_ = new pbc::MapField<string, string>();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public pbc::MapField<string, string> Translations {
get { return translations_; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public override bool Equals(object other) {
return Equals(other as LocalizedString);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public bool Equals(LocalizedString other) {
if (ReferenceEquals(other, null)) {
return false;
}
if (ReferenceEquals(other, this)) {
return true;
}
if (!Translations.Equals(other.Translations)) return false;
return Equals(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public override int GetHashCode() {
int hash = 1;
hash ^= Translations.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
return hash;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public override string ToString() {
return pb::JsonFormatter.ToDiagnosticString(this);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public void WriteTo(pb::CodedOutputStream output) {
translations_.WriteTo(output, _map_translations_codec);
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public int CalculateSize() {
int size = 0;
size += translations_.CalculateSize(_map_translations_codec);
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
return size;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public void MergeFrom(LocalizedString other) {
if (other == null) {
return;
}
translations_.Add(other.translations_);
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public void MergeFrom(pb::CodedInputStream input) {
uint tag;
while ((tag = input.ReadTag()) != 0) {
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 10: {
translations_.AddEntriesFrom(input, _map_translations_codec);
break;
}
}
}
}
}
#endregion
}
I have another message that has a LocalizedString property that I am using for as an input to an rpc function, but I cannot figure out how to set the properties for Translations. In the generated code, Translations is marked as read only.
How does one construct a protobuf message object that contains a map like this in C#?
So after digging through the documentation, I found that Google.Protobuf.Collections.MapField< TKey, TValue > has getters and setters.
The values can by either passing a key, value pair or a dictionary to the Add method of the map.
var localizedName = new LocalizedString();
localizedName.Translations.Add(new Dictionary<string, string>(){
{"ES","Hola"},
{"FR","Bonjour"},
{"JA","こんにちは"},
{"TH","สวัสดี"},
});
localizedName.Translations.Add("ZH_HANS", "你好");
To retrive a value there is a TryGetValue method:
var translation = "";
localizedName.Translations.TryGetValue("TH", out translation);
// translation == "สวัสดี"
In the method MergeFrom, it is giving option to add values in translations in line translations_.Add(other.translations_); :
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public void MergeFrom(LocalizedString other) {
if (other == null) {
return;
}
translations_.Add(other.translations_);
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
To answer "How does one construct a protobuf message object that contains a map like this in C#", Please refer:
https://developers.google.com/protocol-buffers/docs/reference/csharp-generated
https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/collections/map-field-t-key-t-value-
Also there is pull request in github that provides "Proto3 map support for C#" to get the idea how it is implemented:
https://github.com/protocolbuffers/protobuf/pull/543
Also refer these to check if it helps:
Dictionary in protocol buffers
How does Protobuf-net support for Dictionary/KeyValuePair works?
I am aware that the classic way to create a dynamic object is to inherit from DynamicObject. However if I already have a class and I wish to add dynamic properties to subclasses of that then I am stuck.
Say I have a class ReactiveObject And I wish to add dynamic properties to it using DynamicObject. So I do this
public class MyReactiveObject : ReactiveObject, IDynamicMetaObjectProvider{
public DynamicMetaObject GetMetaObject(Expression parameter)
{
...
}
}
I thought the easy way to do this might be to create an instance of DynamicObject and proxy the call to that.
public class MyDynamicObject : DynamicObject{}
public class MyReactiveObject : ReactiveObject, IDynamicMetaObjectProvider{
MyDynamicObject DynamicObject = new MyDynamicObject();
public DynamicMetaObject GetMetaObject(Expression parameter)
{
return this.DynamicObject.GetMetaObject(parameter);
}
}
except that is not going to work because the returned meta object doesn't know anything about the methods on MyReactiveObject. Is there any easy way to do this without fully reimplementing DynamicObject.
I came across the following gist.
https://gist.github.com/breezhang/8954586
public sealed class ForwardingMetaObject : DynamicMetaObject
{
private readonly DynamicMetaObject _metaForwardee;
public ForwardingMetaObject(
Expression expression,
BindingRestrictions restrictions,
object forwarder,
IDynamicMetaObjectProvider forwardee,
Func<Expression, Expression> forwardeeGetter
) : base(expression, restrictions, forwarder)
{
// We'll use forwardee's meta-object to bind dynamic operations.
_metaForwardee = forwardee.GetMetaObject(
forwardeeGetter(
Expression.Convert(expression, forwarder.GetType()) // [1]
)
);
}
// Restricts the target object's type to TForwarder.
// The meta-object we are forwarding to assumes that it gets an instance of TForwarder (see [1]).
// We need to ensure that the assumption holds.
private DynamicMetaObject AddRestrictions(DynamicMetaObject result)
{
var restricted = new DynamicMetaObject(
result.Expression,
BindingRestrictions.GetTypeRestriction(Expression, Value.GetType()).Merge(result.Restrictions),
_metaForwardee.Value
);
return restricted;
}
// Forward all dynamic operations or some of them as needed //
public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
{
return AddRestrictions(_metaForwardee.BindGetMember(binder));
}
public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
{
return AddRestrictions(_metaForwardee.BindSetMember(binder, value));
}
public override DynamicMetaObject BindDeleteMember(DeleteMemberBinder binder)
{
return AddRestrictions(_metaForwardee.BindDeleteMember(binder));
}
public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes)
{
return AddRestrictions(_metaForwardee.BindGetIndex(binder, indexes));
}
public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value)
{
return AddRestrictions(_metaForwardee.BindSetIndex(binder, indexes, value));
}
public override DynamicMetaObject BindDeleteIndex(DeleteIndexBinder binder, DynamicMetaObject[] indexes)
{
return AddRestrictions(_metaForwardee.BindDeleteIndex(binder, indexes));
}
public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
{
return AddRestrictions(_metaForwardee.BindInvokeMember(binder, args));
}
public override DynamicMetaObject BindInvoke(InvokeBinder binder, DynamicMetaObject[] args)
{
return AddRestrictions(_metaForwardee.BindInvoke(binder, args));
}
public override DynamicMetaObject BindCreateInstance(CreateInstanceBinder binder, DynamicMetaObject[] args)
{
return AddRestrictions(_metaForwardee.BindCreateInstance(binder, args));
}
public override DynamicMetaObject BindUnaryOperation(UnaryOperationBinder binder)
{
return AddRestrictions(_metaForwardee.BindUnaryOperation(binder));
}
public override DynamicMetaObject BindBinaryOperation(BinaryOperationBinder binder, DynamicMetaObject arg)
{
return AddRestrictions(_metaForwardee.BindBinaryOperation(binder, arg));
}
public override DynamicMetaObject BindConvert(ConvertBinder binder)
{
return AddRestrictions(_metaForwardee.BindConvert(binder));
}
}
and so I wrote
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using ReactiveUI;
namespace Weingartner.Lens
{
public class Dyno : DynamicObject
{
private readonly DynamicNotifyingObject _D;
public Dyno(DynamicNotifyingObject d)
{
_D = d;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
bool ret = base.TryGetMember(binder, out result);
if (ret == false)
{
result = _D.GetPropertyValue(binder.Name);
if (result != null)
{
ret = true;
}
}
return ret;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
bool ret = base.TrySetMember(binder, value);
if (ret == false)
{
_D.SetPropertyValue(binder.Name, value);
ret = true;
}
return ret;
}
}
And the main object that inherits from ReactiveObject but we can also add dynamic properties to.
/// <summary>
/// An object you can add properties to at runtime which raises INPC events when those
/// properties are changed.
/// </summary>
[DataContract]
public class DynamicNotifyingObject : ReactiveObject, IDynamicMetaObjectProvider
{
#region Private Members
[DataMember]
private Dictionary<string, object> _dynamicProperties;
[DataMember]
private Dictionary<string, Type> _dynamicPropertyTypes;
[IgnoreDataMember]
private Dyno _dynamicObject { get; set; }
public Dyno DynamicObject
{
get
{
lock (this)
{
return _dynamicObject ?? (_dynamicObject = new Dyno(this));
}
}
}
#endregion Private Members
#region Constructor
public DynamicNotifyingObject() : this(new Tuple<string,Type>[] { }) { }
public DynamicNotifyingObject(IEnumerable<Tuple<string,Type>> propertyNames)
{
if (propertyNames == null)
{
throw new Exception("propertyNames is empty");
}
_dynamicProperties = new Dictionary<string, object>();
_dynamicPropertyTypes = new Dictionary<string, Type>();
foreach ( var prop in propertyNames )
{
AddProperty(prop.Item1, prop.Item2);
}
}
#endregion Constructor
#region Public Methods
public void AddProperty<T>( string propertyName, T initialValue )
{
_dynamicProperties.Add(propertyName, initialValue);
_dynamicPropertyTypes.Add(propertyName, typeof(T));
this.RaisePropertyChanged(propertyName);
}
public void AddProperty<T>( string propertyName)
{
AddProperty(propertyName, typeof(T));
}
public void AddProperty( string propertyName, Type type)
{
_dynamicProperties.Add(propertyName, null);
_dynamicPropertyTypes.Add(propertyName, type);
this.RaisePropertyChanged(propertyName);
}
public void SetPropertyValue<T>(string propertyName, T raw)
{
if (!_dynamicProperties.ContainsKey(propertyName))
{
throw new ArgumentException(propertyName + " property does not exist on " + GetType().Name);
}
var converter = DynamicLens2INPC.CreateConverter<T>(raw.GetType(), _dynamicPropertyTypes[propertyName]);
var value = converter(raw);
if (!value.Equals(_dynamicProperties[propertyName]))
{
_dynamicProperties[propertyName] = (object) value;
this.RaisePropertyChanged(propertyName);
}
}
public object GetPropertyValue(string propertyName)
{
if (!_dynamicProperties.ContainsKey(propertyName))
{
throw new ArgumentException(propertyName + " property does not exist " + GetType().Name);
}
return _dynamicProperties.ContainsKey(propertyName) ? _dynamicProperties[propertyName] : null;
}
#endregion Public Methods
DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
{
return new ForwardingMetaObject(parameter, BindingRestrictions.Empty, this, DynamicObject,
// B's meta-object needs to know where to find the instance of B it is operating on.
// Assuming that an instance of A is passed to the 'parameter' expression
// we get the corresponding instance of B by reading the "B" property.
exprA => Expression.Property(exprA, nameof(DynamicObject))
);
}
}
public static class DynamicNotifyingObjectMixin
{
public static TRet RaiseAndSetIfChanged<TObj, TRet>(this TObj This, TRet newValue, ref TRet backingField, [CallerMemberName] string property = "")
where TObj : DynamicNotifyingObject
{
if (EqualityComparer<TRet>.Default.Equals(newValue, backingField))
{
return newValue;
}
This.RaisePropertyChanging(property);
backingField = newValue;
This.RaisePropertyChanged(property);
return newValue;
}
}
}
with a test case
using FluentAssertions;
using Xunit;
namespace Weingartner.Lens.Spec
{
public class DynamicNotifyingObjectSpec
{
class Fixture : DynamicNotifyingObject
{
public Fixture ():
base()
{
this.AddProperty<string>("A");
this.AddProperty<string>("B");
this.SetPropertyValue("A", "AAA");
this.SetPropertyValue("B", "BBB");
}
}
[Fact]
public void ShouldBeAbleToAddPropertiesLaterOn()
{
var ff = new Fixture();
ff.AddProperty<string>("newProp");
ff.AddProperty<string>("XXXX");
dynamic f = ff;
ff.SetPropertyValue("newProp", "CCC");
((object)(f.newProp)).Should().Be("CCC");
f.XXXX = "XXXX";
f.newProp = "DDD";
((object)(f.newProp)).Should().Be("DDD");
((object)(f.XXXX)).Should().Be("XXXX");
}
[Fact]
public void ShouldGenerateNotificationOnPropertyChange()
{
var a = new string []{"A"};
var b = new string []{"B"};
object oa = null;
object ob = null;
var f = new Fixture();
dynamic fd = f;
f.PropertyChanged += (sender, ev) =>
{
dynamic s = sender;
oa = s.A;
ob = s.B;
};
oa.Should().Be(null);
ob.Should().Be(null);
fd.A = "A";
oa.Should().Be("A");
ob.Should().Be("BBB");
fd.B = "B";
oa.Should().Be("A");
ob.Should().Be("B");
}
}
}
Another possibility is just to use this library
https://github.com/remi/MetaObject
using System;
using System.Dynamic;
public class MyClass : Whatever, IDynamicMetaObjectProvider {
// This 1 line is *ALL* you need to add support for all of the DynamicObject methods
public DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression e)
{ return new MetaObject(e, this); }
// Now, if you want to handle dynamic method calls,
// you can implement TryInvokeMember, just like you would in DynamicObject!
public bool TryInvokeMember
(InvokeMemberBinder binder, object[] args, out object result) {
if (binder.Name.Contains("Cool")) {
result = "You called a method with Cool in the name!";
return true;
} else {
result = null;
return false;
}
}
}
and for my particular use case which is inheriting from ReactiveUI.Reactive object and having dynamic properties which support INPC I generated
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using ReactiveUI;
namespace Weingartner.Lens
{
/// <summary>
/// An object you can add properties to at runtime which raises INPC events when those
/// properties are changed.
/// </summary>
[DataContract]
public class DynamicNotifyingObject : ReactiveObject, IDynamicMetaObjectProvider
{
#region Private Members
[DataMember]
private Dictionary<string, object> _DynamicProperties;
[DataMember]
private Dictionary<string, Type> _DynamicPropertyTypes;
#endregion Private Members
#region Constructor
public DynamicNotifyingObject() : this(new Tuple<string,Type>[] { }) { }
public DynamicNotifyingObject(IEnumerable<Tuple<string,Type>> propertyNames)
{
if (propertyNames == null)
{
throw new Exception("propertyNames is empty");
}
_DynamicProperties = new Dictionary<string, object>();
_DynamicPropertyTypes = new Dictionary<string, Type>();
foreach ( var prop in propertyNames )
{
AddProperty(prop.Item1, prop.Item2);
}
}
#endregion Constructor
public void AddProperty<T>( string propertyName, T initialValue )
{
_DynamicProperties.Add(propertyName, initialValue);
_DynamicPropertyTypes.Add(propertyName, typeof(T));
this.RaisePropertyChanged(propertyName);
}
/// <summary>
/// Set the property. Will throw an exception if the property does not exist.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="propertyName"></param>
/// <param name="raw"></param>
public void SetPropertyValue<T>(string propertyName, T raw)
{
if (!_DynamicProperties.ContainsKey(propertyName))
{
throw new ArgumentException(propertyName + " property does not exist on " + GetType().Name);
}
var converter = DynamicLens2INPC.CreateConverter<T>(raw.GetType(), _DynamicPropertyTypes[propertyName]);
var value = converter(raw);
if (!value.Equals(_DynamicProperties[propertyName]))
{
this.RaisePropertyChanging(propertyName);
_DynamicProperties[propertyName] = (object) value;
this.RaisePropertyChanged(propertyName);
}
}
/// <summary>
/// Get the property. Will throw an exception if the property does not exist.
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
public object GetPropertyValue(string propertyName)
{
if (!_DynamicProperties.ContainsKey(propertyName))
{
throw new ArgumentException(propertyName + " property does not exist " + GetType().Name);
}
return _DynamicProperties.ContainsKey(propertyName) ? _DynamicProperties[propertyName] : null;
}
public bool HasDynamicProperty(string propertyName) => _DynamicProperties.ContainsKey(propertyName);
DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression e) { return new MetaObject(e, this); }
/// <summary>
/// Support for MetaObject. See https://github.com/remi/MetaObject
/// </summary>
/// <param name="binder"></param>
/// <param name="result"></param>
/// <returns></returns>
public bool TryGetMember(GetMemberBinder binder, out object result)
{
if (HasDynamicProperty(binder.Name))
{
result = GetPropertyValue(binder.Name);
return true;
}
// This path will return any real properties on the object
result = null;
return false;
}
/// <summary>
/// Support for MetaObject. See https://github.com/remi/MetaObject
/// </summary>
/// <param name="binder"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool TrySetMember(SetMemberBinder binder, object value)
{
if (HasDynamicProperty(binder.Name))
{
SetPropertyValue(binder.Name, value);
return true;
}
// This path will try to set any real properties on the object
return false;
}
}
}
I want to automatically show every IList as expandable in my PropertyGrid (By "expandable", I obviously mean that the items will be shown).
I don't want to use attributes on each list (Once again, I want it to work for EVERY IList)
I tried to achive it by using a custom PropertyDescriptor and an ExpandableObjectConverter. It works, but after I delete items from the list, the PropertyGrid is not being refreshed, still displaying the deleted items.
I tried to use ObservableCollection along with raising OnComponentChanged, and also RefreshProperties attribute, but nothing worked.
This is my code:
public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor
{
private IList _collection;
private readonly int _index = -1;
internal event EventHandler RefreshRequired;
public ExpandableCollectionPropertyDescriptor(IList coll, int idx) : base(GetDisplayName(coll, idx), null)
{
_collection = coll
_index = idx;
}
public override bool SupportsChangeEvents
{
get { return true; }
}
private static string GetDisplayName(IList list, int index)
{
return "[" + index + "] " + CSharpName(list[index].GetType());
}
private static string CSharpName(Type type)
{
var sb = new StringBuilder();
var name = type.Name;
if (!type.IsGenericType)
return name;
sb.Append(name.Substring(0, name.IndexOf('`')));
sb.Append("<");
sb.Append(string.Join(", ", type.GetGenericArguments()
.Select(CSharpName)));
sb.Append(">");
return sb.ToString();
}
public override AttributeCollection Attributes
{
get
{
return new AttributeCollection(null);
}
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get
{
return _collection.GetType();
}
}
public override object GetValue(object component)
{
OnRefreshRequired();
return _collection[_index];
}
public override bool IsReadOnly
{
get { return false; }
}
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 true;
}
public override void SetValue(object component, object value)
{
_collection[_index] = value;
}
protected virtual void OnRefreshRequired()
{
var handler = RefreshRequired;
if (handler != null) handler(this, EventArgs.Empty);
}
}
.
internal class ExpandableCollectionConverter : ExpandableObjectConverter
{
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType)
{
if (destType == typeof(string))
{
return "(Collection)";
}
return base.ConvertTo(context, culture, value, destType);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
IList collection = value as IList;
PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
for (int i = 0; i < collection.Count; i++)
{
ExpandableCollectionPropertyDescriptor pd = new ExpandableCollectionPropertyDescriptor(collection, i);
pd.RefreshRequired += (sender, args) =>
{
var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance);
notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1});
};
pds.Add(pd);
}
// return the property descriptor Collection
return pds;
}
}
And I use it for all ILists with the following line:
TypeDescriptor.AddAttributes(typeof (IList), new TypeConverterAttribute(typeof(ExpandableCollectionConverter)));
Some Clarifications
I want the grid to automatically update when I change the list. Refreshing when another property changes, does not help.
A solution that works, is a solution where:
If you expand the list while it is empty, and then add items, the grid is refreshed with the items expanded
If you add items to the list, expand it, and then remove items (without collapsing), the grid is refreshed with the items expanded, and not throwing ArgumentOutOfRangeException because it is trying to show items that were deleted already
I want this whole thing for a configuration utility. Only the PropertyGrid should change the collections
IMPORTANT EDIT:
I did manage to make the expanded collections update with Reflection, and calling NotifyValueGivenParent method on the context object when the PropertyDescriptor GetValue method is called (when RefreshRequired event is raised):
var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance);
notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1});
It works perfectly, except it causes the event to be raised infinite times, because calling NotifyValueGivenParent causes a reload of the PropertyDescriptor, and therfore, raising the event, and so on.
I tried to solve it by adding a simple flag that will prevent the reloading if it is already reloading, but for some reason NotifyValueGivenParent behaves asynchronously, and therefore the reloading happens after the flag is turned off.
Maybe it is another direction to explore. The only problem is the recursion
There is no need for using ObservableCollection. You can modify your descriptor class as follows:
public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor
{
private IList collection;
private readonly int _index;
public ExpandableCollectionPropertyDescriptor(IList coll, int idx)
: base(GetDisplayName(coll, idx), null)
{
collection = coll;
_index = idx;
}
private static string GetDisplayName(IList list, int index)
{
return "[" + index + "] " + CSharpName(list[index].GetType());
}
private static string CSharpName(Type type)
{
var sb = new StringBuilder();
var name = type.Name;
if (!type.IsGenericType)
return name;
sb.Append(name.Substring(0, name.IndexOf('`')));
sb.Append("<");
sb.Append(string.Join(", ", type.GetGenericArguments()
.Select(CSharpName)));
sb.Append(">");
return sb.ToString();
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get { return this.collection.GetType(); }
}
public override object GetValue(object component)
{
return collection[_index];
}
public override bool IsReadOnly
{
get { return false; }
}
public override string Name
{
get { return _index.ToString(CultureInfo.InvariantCulture); }
}
public override Type PropertyType
{
get { return collection[_index].GetType(); }
}
public override void ResetValue(object component)
{
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
public override void SetValue(object component, object value)
{
collection[_index] = value;
}
}
Instead of the ExpandableCollectionConverter I would derive the CollectionConverter class, so you can still use the ellipsis button to edit the collection in the old way (so you can add/remove items if the collection is not read-only):
public class ListConverter : CollectionConverter
{
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
IList list = value as IList;
if (list == null || list.Count == 0)
return base.GetProperties(context, value, attributes);
var items = new PropertyDescriptorCollection(null);
for (int i = 0; i < list.Count; i++)
{
object item = list[i];
items.Add(new ExpandableCollectionPropertyDescriptor(list, i));
}
return items;
}
}
And I would use this ListConverter on the properties where I want to see expandable list. Of course, you can register the type converter generally as you do in your example, but that overrides everything, which might not be overall intended.
public class MyClass
{
[TypeConverter(typeof(ListConverter))]
public List<int> List { get; set; }
public MyClass()
{
List = new List<int>();
}
[RefreshProperties(RefreshProperties.All)]
[Description("Change this property to regenerate the List")]
public int Count
{
get { return List.Count; }
set { List = Enumerable.Range(1, value).ToList(); }
}
}
Important: The RefreshProperties attribute should be defined for the properties that change other properties. In this example, changing the Count replaces the whole list.
Using it as propertyGrid1.SelectedObject = new MyClass(); produces the following result:
I don't want it to refresh when other property refreshes. I want it to refresh when the list is changed. I add items to the list, expand it, add more items, but the items are not updated
This is a typical misuse of PropertyGrid. It is for configuring a component, and not for reflecting the concurrent changes on-the-fly by an external source. Even wrapping the IList into an ObservableCollection will not help you because it is used only by your descriptor, while the external source manipulates directly the underlying IList instance.
What you can still do is an especially ugly hack:
public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor
{
// Subscribe to this event from the form with the property grid
public static event EventHandler CollectionChanged;
// Tuple elements: The owner of the list, the list, the serialized content of the list
// The reference to the owner is a WeakReference because you cannot tell the
// PropertyDescriptor that you finished the editing and the collection
// should be removed from the list.
// Remark: The references here may survive the property grid's life
private static List<Tuple<WeakReference, IList, byte[]>> collections;
private static Timer timer;
public ExpandableCollectionPropertyDescriptor(ITypeDescriptorContext context, IList collection, ...)
{
AddReference(context.Instance, collection);
// ...
}
private static void AddReference(object owner, IList collection)
{
// TODO:
// - serialize the collection into a byte array (BinaryFormatter) and add it to the collections list
// - if this is the first element, initialize the timer
}
private static void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
// TODO: Cycle through the collections elements
// - If WeakReference is not alive, remove the item from the list
// - Serialize the list again and compare the result to the last serialized content
// - If there a is difference:
// - Update the serialized content
// - Invoke the CollectionChanged event. The sender is the owner (WeakReference.Target).
}
}
Now you can use it like this:
public class Form1 : Form
{
MyObject myObject = new MyObject();
public MyForm()
{
InitializeComponent();
ExpandableCollectionPropertyDescriptor.CollectionChanged += CollectionChanged();
propertyGrid.SelectedObject = myObject;
}
private void CollectionChanged(object sender, EventArgs e)
{
if (sender == myObject)
propertyGrid.SelectedObject = myObject;
}
}
But honestly, I would not use it at all. It has serious flaws:
What if a collection element is changed by the PropertyGrid, but the timer has not updated the last external change yet?
The implementer of the IList must be serializable
Ridiculous performance overhead
Though using weak references may reduce memory leaks, it does not help if the objects to edit have longer life cycle than the editor form, because they will remain in the static collection
Putting it all together, this works:
Here is the class with the lists that we will put an instance of in our property grid. Also to demonstrate usage with a list of a complex object, I have the NameAgePair class.
public class SettingsStructure
{
public SettingsStructure()
{
//To programmatically add this to properties that implement ILIST for the naming of the edited node and child items:
//[TypeConverter(typeof(ListConverter))]
TypeDescriptor.AddAttributes(typeof(IList), new TypeConverterAttribute(typeof(ListConverter)));
//To programmatically add this to properties that implement ILIST for the refresh and expansion of the edited node
//[Editor(typeof(CollectionEditorBase), typeof(System.Drawing.Design.UITypeEditor))]
TypeDescriptor.AddAttributes(typeof(IList), new EditorAttribute(typeof(CollectionEditorBase), typeof(UITypeEditor)));
}
public List<string> ListOfStrings { get; set; } = new List<string>();
public List<string> AnotherListOfStrings { get; set; } = new List<string>();
public List<int> ListOfInts { get; set; } = new List<int>();
public List<NameAgePair> ListOfNameAgePairs { get; set; } = new List<NameAgePair>();
}
public class NameAgePair
{
public string Name { get; set; } = "";
public int Age { get; set; } = 0;
public override string ToString()
{
return $"{Name} ({Age})";
}
}
Here is the ListConverter class to handle making the child nodes.
public class ListConverter : CollectionConverter
{
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
IList list = value as IList;
if (list == null || list.Count == 0)
return base.GetProperties(context, value, attributes);
var items = new PropertyDescriptorCollection(null);
for (int i = 0; i < list.Count; i++)
{
object item = list[i];
items.Add(new ExpandableCollectionPropertyDescriptor(list, i));
}
return items;
}
public override object ConvertTo(ITypeDescriptorContext pContext, CultureInfo pCulture, object value, Type pDestinationType)
{
if (pDestinationType == typeof(string))
{
IList v = value as IList;
int iCount = (v == null) ? 0 : v.Count;
return $"({iCount} Items)";
}
return base.ConvertTo(pContext, pCulture, value, pDestinationType);
}
}
Here is the ExpandableCollectionPropertyDescriptor class for the individual items.
public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor
{
private IList _Collection;
private readonly int _Index;
public ExpandableCollectionPropertyDescriptor(IList coll, int idx) : base(GetDisplayName(coll, idx), null)
{
_Collection = coll;
_Index = idx;
}
private static string GetDisplayName(IList list, int index)
{
return "[" + index + "] " + CSharpName(list[index].GetType());
}
private static string CSharpName(Type type)
{
var sb = new StringBuilder();
var name = type.Name;
if (!type.IsGenericType) return name;
sb.Append(name.Substring(0, name.IndexOf('`')));
sb.Append("<");
sb.Append(string.Join(", ", type.GetGenericArguments().Select(CSharpName)));
sb.Append(">");
return sb.ToString();
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get { return this._Collection.GetType(); }
}
public override object GetValue(object component)
{
return _Collection[_Index];
}
public override bool IsReadOnly
{
get { return false; }
}
public override string Name
{
get { return _Index.ToString(CultureInfo.InvariantCulture); }
}
public override Type PropertyType
{
get { return _Collection[_Index].GetType(); }
}
public override void ResetValue(object component)
{
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
public override void SetValue(object component, object value)
{
_Collection[_Index] = value;
}
}
And then the CollectionEditorBase class for refreshing the property grid after the collection editor is closed.
public class CollectionEditorBase : CollectionEditor
{
protected PropertyGrid _PropertyGrid;
private bool _ExpandedBefore;
private int _CountBefore;
public CollectionEditorBase(Type type) : base(type) { }
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
//Record entry state of property grid item
GridItem giThis = (GridItem)provider;
_ExpandedBefore = giThis.Expanded;
_CountBefore = (giThis.Value as IList).Count;
//Get the grid so later we can refresh it on close of editor
PropertyInfo piOwnerGrid = provider.GetType().GetProperty("OwnerGrid", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
_PropertyGrid = (PropertyGrid)piOwnerGrid.GetValue(provider);
//Edit the collection
return base.EditValue(context, provider, value);
}
protected override CollectionForm CreateCollectionForm()
{
CollectionForm cf = base.CreateCollectionForm();
cf.FormClosing += delegate (object sender, FormClosingEventArgs e)
{
_PropertyGrid.Refresh();
//Because nothing changes which grid item is the selected one, expand as desired
if (_ExpandedBefore || _CountBefore == 0) _PropertyGrid.SelectedGridItem.Expanded = true;
};
return cf;
}
protected override object CreateInstance(Type itemType)
{
//Fixes the "Constructor on type 'System.String' not found." when it is an empty list of strings
if (itemType == typeof(string)) return string.Empty;
else return Activator.CreateInstance(itemType);
}
}
Now the usage produces:
And performing various operations produces:
You can tweak it to operate like you like.
I'm using NHibernate with Postgresql as the backend and had to create custom types for converting System.DateTime to Postgresql "time" types as well as System.TimeSpans to "interval" db types. The IUserType I created are working and are being applied for reads and updates but they are never being applied when I try to insert an object into the database. I've set breakpoints in the IUserTypes but they never are hit when an insert is happening.
I think the issue may be around the fact that the object is just a POCO object and isn't a proxy yet so the mapping that applies the transformation to the usertype doesn't happen when I set the value of my property.
If I do an update and set one of these properties I can see the breakpoints in the IUserType are fired.
Any thoughts?
UPDATE
The Equals Method of the IUserType is being called but the NullSafeSet where I do the necessary conversion is not.
EDIT
Added code samples
public class TimeType : BaseImmutableUserType<TimeSpan>
{
public override object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var val = NHibernateUtil.Time.NullSafeGet(rs, names[0]);
if (val == null)
return null;
var dt = DateTime.Parse(val.ToString());
return new TimeSpan(0, dt.Hour, dt.Minute, dt.Second);
}
public override void NullSafeSet(IDbCommand cmd, object value, int index)
{
var obj = (TimeSpan)value;
((IDbDataParameter) cmd.Parameters[index]).Value = obj;
}
public override SqlType[] SqlTypes
{
get
{
return new[] { SqlTypeFactory.Time };
}
}
}
public abstract class BaseImmutableUserType<T> : IUserType
{
public abstract object NullSafeGet(IDataReader rs, string[] names, object owner);
public abstract void NullSafeSet(IDbCommand cmd, object value, int index);
public abstract SqlType[] SqlTypes { get; }
public BaseImmutableUserType()
{
int i = 0;
}
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x == null || y == null)
{
return false;
}
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object DeepCopy(object value)
{
return value;
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public Type ReturnedType
{
get { return typeof(T); }
}
public bool IsMutable
{
get { return false; }
}
}
I found the following two options:
Option 1: do not use your own UserType. Instead use NHibernate's own TimeAsTimeSpan like this:
Map(x => x.TimeFrom)
.CustomType("TimeAsTimeSpan");
(Example taken from here)
Option 2: Modify your class a little:
public class TimeType : BaseImmutableUserType<TimeSpan>
{
// this is taken from the source of NHibernate.Type.TimeAsTimeSpanType
private static readonly DateTime BaseDateValue = new DateTime(1753, 01, 01);
public override object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var val = NHibernateUtil.TimeAsTimeSpan.NullSafeGet(rs, names[0]);
if (val == null)
return null;
var dt = DateTime.Parse(val.ToString());
return new TimeSpan(0, dt.Hour, dt.Minute, dt.Second);
}
public override void NullSafeSet(IDbCommand cmd, object value, int index)
{
//var obj = (TimeSpan)value; // we can't use TimeSpan here but need to use DateTime
// this is taken from the source of NHibernate.Type.TimeAsTimeSpanType
DateTime date = BaseDateValue.AddTicks(((TimeSpan)value).Ticks);
((IDbDataParameter)cmd.Parameters[index]).Value = date;
}
public override SqlType[] SqlTypes
{
get
{
return new[] { NHibernate.SqlTypes.SqlTypeFactory.Time };
}
}
}
So I've got a ConfigurationSection/ConfigurationElementCollection that has a configuration like this:
<mimeFormats>
<add mimeFormat="text/html" />
</mimeFormats>
And here is how I handle the mimeFormats:
public class MimeFormatElement: ConfigurationElement
{
#region Constructors
/// <summary>
/// Predefines the valid properties and prepares
/// the property collection.
/// </summary>
static MimeFormatElement()
{
// Predefine properties here
_mimeFormat = new ConfigurationProperty(
"mimeFormat",
typeof(MimeFormat),
"*/*",
ConfigurationPropertyOptions.IsRequired
);
}
private static ConfigurationProperty _mimeFormat;
private static ConfigurationPropertyCollection _properties;
[ConfigurationProperty("mimeFormat", IsRequired = true)]
public MimeFormat MimeFormat
{
get { return (MimeFormat)base[_mimeFormat]; }
}
}
public class MimeFormat
{
public string Format
{
get
{
return Type + "/" + SubType;
}
}
public string Type;
public string SubType;
public MimeFormat(string mimeFormatStr)
{
var parts = mimeFormatStr.Split('/');
if (parts.Length != 2)
{
throw new Exception("Invalid MimeFormat");
}
Type = parts[0];
SubType = parts[1];
}
}
And obviously I need a TypeConverter that actually does something (instead of this empty shell):
public class MimeFormatConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
throw new NotImplementedException();
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
throw new NotImplementedException();
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
throw new NotImplementedException();
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
throw new NotImplementedException();
}
}
How do I set up a TypeConverter that will allow type conversion from/to string? I've tried using the MSDN examples but I keep getting error message:
TypeConverter cannot convert from System.String.
Essentially, how can it be set up so that it will just work with whatever ConfigurationSection is trying to do?
You can put TypeConverterAttribute on the property to tell the serializer how to handle it.
[TypeConverter(typeof(MimeFormatConverter))]
[ConfigurationProperty("mimeFormat", IsRequired = true)]
public MimeFormat MimeFormat
{
get { return (MimeFormat)base[_mimeFormat]; }
}
Try this:
TestSection.cs
public class TestSection : ConfigurationSection
{
private static readonly ConfigurationProperty sFooProperty = new ConfigurationProperty("Foo",
typeof(Foo),
null,
new FooTypeConverter(),
null,
ConfigurationPropertyOptions.None);
public static readonly ConfigurationPropertyCollection sProperties = new ConfigurationPropertyCollection();
static TestSection()
{
sProperties.Add(sFooProperty);
}
public Foo Foo
{
get { return (Foo)this[sFooProperty]; }
set { this[sFooProperty] = value; }
}
protected override ConfigurationPropertyCollection Properties
{
get { return sProperties; }
}
}
Foo.cs
public class Foo
{
public string First { get; set; }
public string Second { get; set; }
public override string ToString()
{
return First + ',' + Second;
}
}
FooTypeConverter.cs
public class FooTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return (sourceType == typeof(string));
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string val = value as string;
if (val != null)
{
string[] parts = val.Split(',');
if (parts.Length != 2)
{
// Throw an exception
}
return new Foo { First = parts[0], Second = parts[1] };
}
return null;
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return (destinationType == typeof(string));
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
Foo val = value as Foo;
if (val != null)
return val.ToString();
return null;
}
}
I figured it out. Here is the solution:
public class MimeFormatElement: ConfigurationElement
{
#region Constructors
/// <summary>
/// Predefines the valid properties and prepares
/// the property collection.
/// </summary>
static MimeFormatElement()
{
// Predefine properties here
_mimeFormat = new ConfigurationProperty(
"mimeFormat",
typeof(MimeFormat),
"*/*",
ConfigurationPropertyOptions.IsRequired
);
_properties = new ConfigurationPropertyCollection {
_mimeFormat, _enabled
};
}
private static ConfigurationProperty _mimeFormat;
private static ConfigurationPropertyCollection _properties;
[ConfigurationProperty("mimeFormat", IsRequired = true)]
public MimeFormat MimeFormat
{
get { return (MimeFormat)base[_mimeFormat]; }
}
}
/*******************************************/
[TypeConverter(typeof(MimeFormatConverter))]
/*******************************************/
public class MimeFormat
{
public string Format
{
get
{
return Type + "/" + SubType;
}
}
public string Type;
public string SubType;
public MimeFormat(string mimeFormatStr)
{
var parts = mimeFormatStr.Split('/');
if (parts.Length != 2)
{
throw new Exception("Invalid MimeFormat");
}
Type = parts[0];
SubType = parts[1];
}
}
public class MimeFormatConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return new MimeFormat((string)value);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var val = (MimeFormat)value;
return val.Type + "/" + val.SubType;
}
}
From this point, you have to create the convert sections within the ConvertTo and ConvertFrom methods
public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo culture, object value ) {
if ( value == null )
return null;
try {
if ( value is string ) {
string s = (string)value;
// here is where you look at the string to figure out the MimeFormat
// like so....
return new MimeFormat( s );
}
throw new NotSupportedException( NotSupportedException( value.GetType(), typeof(MimeFormat) );
}
public override object ConvertTo( ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType ) {
if ( value == null )
return null;
MimeFormat p = (MimeFormat)value;
if ( destinationType == typeof( String ) )
return p.ToString();
throw new NotSupportedException( NotSupportedException( typeof(MimeFormat), destinationType ) );
}
EDITED
You also need to override the CanConvert functions as well.
public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType ) {
if ( sourceType == typeof( string ) )
return true;
return false;
}
public override bool CanConvertTo( ITypeDescriptorContext context, Type destinationType ) {
if ( destinationType == typeof( string ) )
return true;
return false;
}