none
PropertyGrid difference between .NET 1.0 and .Net 2.0 with multiple objects selected RRS feed

  • Question

  • Hi,

    I have noticed that the property grid behaves differently in .Net 2.0 than it used to in .Net 1.0.  When there are multiple objects selected, and an object property value is changed in the grid, it is assigning the same object instance to each of the selected objects in .Net 1.0, but in .Net 2.0 it is assigning different instances.  From debugging and investigating with Reflector, it looks like PropertyGridInternal.MergePropertyDescriptor.SetValue is the culprit -- the code there is calling CopyValue to copy the value, then setting the copy into the property for each selected object.  At first I thought this was due to the fact that the object type being assigned implements ICloneable (maybe the new PropertyGrid control is checking for ICloneable and making copies of the object to assign to each selected object) but removing the ICloneable implementation does not make it work like .Net 1.0.  That just makes it call into my type converter and clone the object by converting it to a string and back again.  Can anyone tell me how to make the property grid work like it did in .Net 1.0?

    This is a big issue because it changes how the code for my object gets generated in the forms.  In .Net 1.0, it correctly creates exactly one instance, then generates code that creates this instances and assigns it to the appropriate property in each object, but in .Net 2.0 it generates code that creates lots of copies of the object.  Also, in .NET 2.0 as soon as you change the property value by selecting an item from the list generated by the type converter, the property grid shows a blank in the value portion and makes the property expandable, but when the property is expanded, nothing happens except the expand indicator ("+") disappears.

    Is there some kind of new attribute that needs to be used to get the old behavior back?  Or is this a BUG in the property grid in .Net 2.0?  It certainly looks like one, with the disappearing expand indicator on the property.
    Wednesday, May 30, 2007 9:39 PM

Answers

  • Hello CodeFreq,

    well, I'm not sure. Let's say you have a 3d modelling tool. You select a cube and a ball and you see their common properties in the PropertyGrid. The grid is just a visual editor. One of the properties is an instance to a texture class. I click on the button of the property and I edit let's say a base color. If the ball and the cube share this new texture instance, and if you edit a subcomponent of this texture somewhere else in the app for the ball, you will also change it for the cube without knowing it. So for me it's more logical to have several instances in a complex property even if they are equal.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker


    Thursday, May 31, 2007 9:40 PM

All replies

  • Hi,

    It seems you are right. I looked into the Microsoft code and I come to the same conclusion. And I see nowhere a switch to change this behavior.
    However I found the CopyValue very interesting and I think that I will add this option in Smart PropertyGrid.Net (which behaves as the .Net 1.1 PropertyGrid, if you are interested). This way, one will be able to choose between cloning each value for each target instance or not.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker

    Thursday, May 31, 2007 7:42 PM
  • Thanks, I will have to check out your Smart PropertyGrid.

    Can anyone confirm whether this is a bug in the .Net 2.0 PropertyGrid control?
    Thursday, May 31, 2007 7:49 PM
  • well, the way it is written, I think this is simply by design. Imagine the different target instances at first. Logically, for a given common property, they should have different values (i.e. different references to instances of the property type). So when the user sets a new value in a property, MS surely wants to apply the same logic and keep a kind of independence between the target instances.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker

    Thursday, May 31, 2007 8:16 PM
  • It seems to me, if I understand correctly, is that there should be a common instance for the target objects until a difference occurs.
    If I assign the property in the grid to several target objects, there should only be one instance of that object until I change a
    sub property for one or more that creates a heterogeneous group and thus diverges from a single instance.
    I think this is how .Net 1.0 and 1.1 worked and the .Net 2.0 framework has changed the behavior.
    I agree that it may have been changed by design but I do not think it is correct.
    I would deem this a bug, if I implemented a .Net 1.1 version and a .Net 2.0 version of the same object / control.
    And I think my customers would, too.
    Thursday, May 31, 2007 9:13 PM
  • Hello CodeFreq,

    well, I'm not sure. Let's say you have a 3d modelling tool. You select a cube and a ball and you see their common properties in the PropertyGrid. The grid is just a visual editor. One of the properties is an instance to a texture class. I click on the button of the property and I edit let's say a base color. If the ball and the cube share this new texture instance, and if you edit a subcomponent of this texture somewhere else in the app for the ball, you will also change it for the cube without knowing it. So for me it's more logical to have several instances in a complex property even if they are equal.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker


    Thursday, May 31, 2007 9:40 PM
  • Nicolas,

    That is understandable and for the sake of argument let's say we are working with only
    two objects in the property grid (to keep it simple).
    If both items are the same class and have the same properties, there needs to be a
    mechanism to represent in the property grid that those two items are the same.
    If I have a designtime typeconverter attached to the property that represents the
    object as a string, say the object type, like"Cube"
    and both items are cubes and have the same underlying properties but yet have
    different instances, they are still represented in the .Net 2.0 propertygrid as null
    string or "disjoint".
    It would stand to reason that there should be a way for the object developer to
    control that representation with a deep or shallow comparison.
    The consumer of the objects would not be able to set common sub properties
    or even identify the objects are the same.

    Just a few thoughts.
    CodeFreq
    Friday, June 1, 2007 1:21 PM
  •  VisualHint wrote:
    Hello CodeFreq,

    well, I'm not sure. Let's say you have a 3d modelling tool. You select a cube and a ball and you see their common properties in the PropertyGrid. The grid is just a visual editor. One of the properties is an instance to a texture class. I click on the button of the property and I edit let's say a base color. If the ball and the cube share this new texture instance, and if you edit a subcomponent of this texture somewhere else in the app for the ball, you will also change it for the cube without knowing it. So for me it's more logical to have several instances in a complex property even if they are equal.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker




    Nicolas,

    It is my understanding that the TypeConverter attribute on the property should take care of that situation. We have created a type converter that is designed to handle displaying a list of object types which implement the interface for the property, and to create a new instance of the object type when its properties are edited. More specifically, the type converter overrides GetStandardValues to return a StandardValuesCollection which contains instances of the type which the type converter creates in a static array. Those instances are then each passed into ConvertTo to make them into strings to display in the list, and when an item is selected from the list, ConvertFrom is called to make that into an instance of the object. When the objects sub properties are edited in the property grid, then GetCreateInstanceSupported is called, which returns true, so the property values are passed into CreateInstance, where our code recreates a new instance of the appropriate type, using the context to determine the current value of the property, and sets its properties according to the IDictionary of property values passed in.

    This all works great in our product in .Net 1.0 and .Net 1.1. We have a grid, and Cell objects, and each Cell object has a CellType property of type ICellType. When the user selects a range of cells in the grid, then our designers set an array of Cell objects into the PropertyGrid.SelectedObjects, and the user can select a particular ICellType implementation that we provide from the drop-down list. The drop-down list is created by the property grid from the StandardValuesCollection we return from the type converter, apparently by passing each instance back into the type converter's ConvertTo to convert it to a string to display in the list. Selecting an item in the list causes a new instance of our class to be created in the type converter's ConvertFrom implementation (from the string value in the list assigned to the property) and assigned to the CellType property of each Cell object.

    This instance is indeed shared between each such Cell object, and this is exactly what we intend to happen. When the designer is closed and the code for the form viewed, there is exactly one instance of our ICellType implementation being created in the form code (since the type converter can also convert to InstanceDescriptor), then initialized in the form code after the constructor is called, and then each appropriate Cell object is assigned a reference to that ICellType instance variable in the code.

    When the user opens the designer again, and selects a subset of the Cell objects that were just assigned the cell type, the property grid shows the common properties of the SelectedObjects (all objects are Cells so all properties are common) and it also shows the CellType property with a common value for all instances of Cell displayed in the property grid, as it should. The CellType property can be expanded and its properties inspected and changed, and when that happens, our type converter recreates the instance of the CellType object in its CreateInstance override, by getting the current value from the context, cloning it, settings the properties on the clone according to the IDictionary passed in, and then returning the new instance. Each Cell object selected will now have that new instance assigned to its CellType property. On closing the designer and viewing the code, there are now exactly two instance of our ICellType implementation created in the code, and each is assigned to the appropriate Cell objects.

    In .Net 2.0, this will not work because each Cell object in PropertyGrid.SelectedObjects is getting assigned a different (but otherwise identical) instance of our CellType object, so after setting the property, the property grid will not show the CellType object at all since the same instance is not in each Cell object. The property cannot be expanded, and it sub-properties cannot be edited, unless you select a single Cell object and make changes to that particular instance of its CellType object. There appears to be no way to force the same instance into each Cell object, since the PropertyGrid insists on making these identical copies of each object before it assigns it to each Cell. That CopyValue code is first trying to use ICloneable to do it, and if that fails it tries to use the type converter to convert the object into an InstanceDescriptor and invoke that to do it, and if that fails then it tries to use the type converter to convert the instance to a string and then convert the string back into a new instance. I do not see a way to make it do what we want.

    Does this make it more clear what the problem is? It is now impossible to make a common instance get assigned to an array of objects displayed in the property grid, and I do not think that is correct since it breaks backwards compatibility with the old property grid. I do not think it is correct also because it results in redundant code generated in the form, with a proliferation of copies of these otherwise identical objects being created in the form code and assigned separately, when the same results could be achieved with less code and fewer object instances.

    I would absolutely LOVE it if somebody could tell me some simple thing that we can do to make this work.
    Friday, June 1, 2007 3:31 PM
  • Hummm, interesting. I understand your point about optimizing the number of instances of CellType. However I still think my example is valuable. The conclusion should be that for a given property, the behavior should be switchable.

    On the other hand I don't understand the fact that you don't see the CellType property anymore even when they are identical in your 2 cells. In Reflector I already checked that the grid only relies on the Type and the Name of the property to know if a property must be shown (this happens in PropertyDescriptor.Equals), and to make Smart PropertyGrid compatible with the MSPG I adopted this behavior too. Maybe if you have time you could check this with the eval of SPG and see what happens.

    If you want to send me a small sample to reproduce the issue I will check in debug mode what happens (use the helpdesk of the visualhint web site to send it).

    Hope this helps a bit.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker


    Sunday, June 3, 2007 2:17 AM
  • I have found a solution and I hope it will be applicable to your architecture. You will find it at this blog post where I was able to attach a small sample showing the two different behaviors. If you see any mistake, typo or whatever, just comment there.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker

    Monday, June 4, 2007 12:56 AM
  • Nicolas,

    Sorry for the delayed reply, and thank you SO MUCH for looking into this more with me! I really appreciate you taking the time to look into this with me in such detail. I still can't believe I inspired a blog post!

    I have investigated this some more and put together a simple example without all the baggage of our commercial product in the way. My example consists of two projects, PropertyGridBugSampleControl and PropertyGridBugApp. The first is a C# control project that defines a control, a property in that control using an interface type, and a number of classes which implement the interface, along with a type converter for displaying a list of those class names and creating instances of those classes. The second project is a C# application that defines a form containing a number of instances of the control, and illustrates the problem when you edit the form in design mode. Selecting a set of instances of the control on the form and then setting the sample property in the property grid shows the problem behavior clearly -- after selecting a type from the drop-down list for the property, you cannot expand the property or edit its sub-property values.

    I found in making this sample that there is also a bug in .Net 1.0 and 1.1 which I had not remembered working around, but is necessary to work around to make this work. The bug is in InstanceDescriptorCodeDomSerializer, and to work around it the classes implementing the interface for the property must use a custom CodeDomSerializer that checks whether the object being serialized has already been serialized to the form code and returns a CodeVariableReferenceExpression instead of delegating the serialization to the default implementation. Maybe this is why Microsoft decided to make the change with the CopyValue code in the new PropertyGrid for .Net 2.0. Without this custom serializer, InstanceDescriptorCodeDomSerializer will serialize each reference to the same instance separately and generate code defining and initializing the same variable names more than once, which will not compile.

    I think CopyValue is a real problem that needs to be addressed by Microsoft. That code is making it impossible to make the property grid behave correctly with object properties that use a type converter to display a list of default types and create them at design time, and I cannot find an acceptable work around. Your suggestion will not work because it does not address the cause of the problem: the CopyValue code will still copy the objects if it can, even if the type converter returns the same instance each time a particular string is passed to ConvertFrom.

    The problem is much more apparent in design mode using lots of copies of a control on a form and selecting sets of instances of those controls to set a property. This makes the problem much more difficult to investigate, however, since breakpoints in the type converter code are hit very frequently when the PropertyGrid has an instance in its SelectedObjects collection. I have found that if I cripple the classes implementing the interface for my property, so that they are unable to be copied at all, then it works around the problem. This workaround requires that the classes (1) do not implement ICloneable, (2) do not have a TypeConverter capable of converting them into complete InstanceDescriptor objects, (3) do not have a TypeConverter capable of converting them to and from strings, and (4) are not marked with the Serializable attribute and thus cannot be saved with a BinaryFormatter.

    To see the problem, take this code and compile it with Visual Studio 2002 or 2003, then try opening the form designer, selecting several instances of the control, and setting the sample property to one of the items in the list. You will see the property grid work correctly, and the value will show in the grid and can be expanded and edited, and its sub-property set to cause the object to be recreated with the new value. Then look at the form code and see how the code has exactly one instance of the appropriate type being created, and initialized before the field is used, and then assigned using that field to each appropriate control object. Then go back to the designer and select a subset of the controls that were previously selected, and change the sample property, either by expanding it and changing its sub-property or by selecting a new type from the list. Then expand the property and set its sub-property, and look at the form code again. It should now show exactly two instances of the appropriate types, each with their own field defined, and each should be initialized before the field is used, and each field should be assigned to the appropriate instances of the control.

    Now open Visual Studio 2005 and open the sample application in that environment, and repeat the above tests. You will see the property grid work incorrectly, since each time you try to set the sample property with multiple instances of the control selected, each instance will be assigned a different copy of the selected type. Since each instance has a different value for the property, the property grid does not show anything for the property value, and the property cannot be expanded nor edited (even though it shows the expand icon). Viewing the form code shows that each instance of the control is initialized with a different copy of the selected type.

    Here is the code for the control project I am testing with:

    using System;
    using System.CodeDom;
    using System.ComponentModel;
    using System.ComponentModel.Design.Serialization;
    using System.Collections;
    using System.Drawing;
    using System.Globalization;
    using System.Reflection;
    using System.Text;

    namespace PropertyGridBugSampleControl
    {
    public class PropertyGridBugSampleControl : Control
    {

    private ISampleProperty property = null;

    public PropertyGridBugSampleControl()
    {
    }
    [TypeConverter(typeof(SampleTypeConverter))]
    [RefreshProperties(RefreshProperties.All)]
    public ISampleProperty SampleProperty
    {
    get
    {
    return property;
    }
    set
    {
    property = value;
    }
    }
    protected override void OnPaint(PaintEventArgs e)
    {
    base.OnPaint(e);
    if (Site != null)
    e.Graphics.DrawString(Site.Name, Font, Brushes.Black, (RectangleF)ClientRectangle);
    }
    }
    // interface for sample property
    public interface ISampleProperty
    {
    }
    [Serializable()]
    [TypeConverter(typeof(SampleTypeConverter))]
    [DesignerSerializer(typeof(PropertyGridBugSampleControlSerialization.InstanceDescriptorCodeDomSerializer), typeof(CodeDomSerializer))]
    public class SamplePropertyBase : ICloneable
    {
    public virtual object Clone()
    {
    return MemberwiseClone();
    }
    }
    // three implementations of ISampleProperty, each with a different subproperty
    [Serializable()]
    public class SamplePropertyType1 : SamplePropertyBase, ISampleProperty
    {
    private Color color = Color.Empty;
    [NotifyParentProperty(true)]
    public Color ColorProperty
    {
    get
    {
    return color;
    }
    set
    {
    color = value;
    }
    }
    }
    [Serializable()]
    public class SamplePropertyType2 : SamplePropertyBase, ISampleProperty
    {
    private string str = null;
    [NotifyParentProperty(true)]
    public string StringProperty
    {
    get
    {
    return str;
    }
    set
    {
    str = value;
    }
    }
    }
    [Serializable()]
    public class SamplePropertyType3 : SamplePropertyBase, ISampleProperty
    {
    public enum EnumPropertyValues
    {
    Enum1,
    Enum2,
    Enum3
    }
    private EnumPropertyValues enumprop = EnumPropertyValues.Enum1;
    [NotifyParentProperty(true)]
    public EnumPropertyValues EnumProperty
    {
    get
    {
    return enumprop;
    }
    set
    {
    enumprop = value;
    }
    }
    }
    // type converter for ISampleProperty
    class SampleTypeConverter : TypeConverter
    {
    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
    {
    if (value is ISampleProperty)
    return TypeDescriptor.GetProperties(value, attributes);
    return base.GetProperties(value);
    }
    public override bool GetPropertiesSupported(ITypeDescriptorContext context)
    {
    return true;
    }
    public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
    {
    return new StandardValuesCollection(new object[] {null, new SamplePropertyType1(), new SamplePropertyType2(), new SamplePropertyType3()});
    }
    public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
    {
    return true;
    }
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
    if (sourceType == typeof(string))
    return true;
    return base.CanConvertFrom(context, sourceType);
    }
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
    if (value is string)
    {
    string v = (string)value;
    if (v == null || v.Length == 0 || v.Equals("(none)"))
    return null;
    else if (v.Equals("SamplePropertyType1"))
    return new SamplePropertyType1();
    else if (v.Equals("SamplePropertyType2"))
    return new SamplePropertyType2();
    else if (v.Equals("SamplePropertyType3"))
    return new SamplePropertyType3();
    }
    return base.ConvertFrom(context, culture, value);
    }
    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
    if (destinationType.Equals(typeof(string)) || destinationType.Equals(typeof(InstanceDescriptor)))
    return true;
    return base.CanConvertTo(context, destinationType);
    }
    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
    if (destinationType == typeof(string))
    {
    if (value == null)
    return "(none)";
    else if (value is SamplePropertyType1)
    return "SamplePropertyType1";
    else if (value is SamplePropertyType2)
    return "SamplePropertyType2";
    else if (value is SamplePropertyType3)
    return "SamplePropertyType3";
    }
    else if (destinationType == typeof(InstanceDescriptor) && value is ISampleProperty)
    {
    ConstructorInfo ci = value.GetType().GetConstructor(Type.EmptyTypes);
    if (ci != null)
    return new InstanceDescriptor(ci, null, false);
    }
    return base.ConvertTo(context, culture, value, destinationType);
    }
    public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
    {
    object ret = null;
    if (context != null)
    {
    object current = context.PropertyDescriptor.GetValue(context.Instance);
    if (current is SamplePropertyType1)
    ret = new SamplePropertyType1();
    else if (current is SamplePropertyType2)
    ret = new SamplePropertyType2();
    else if (current is SamplePropertyType3)
    ret = new SamplePropertyType3();
    if (ret != null)
    {
    PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(current, new Attribute[] { new BrowsableAttribute(true) });
    PropertyDescriptor pd;
    foreach (DictionaryEntry e in propertyValues)
    {
    pd = pdc[(string)e.Key];
    if (pd != null)
    pd.SetValue(ret, e.Value);
    }
    }
    else
    ret = current;
    }
    return ret;
    }
    public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
    {
    return true;
    }
    }
    public class InstanceDescriptorCodeDomSerializer : CodeDomSerializer
    {
    public override object Deserialize(IDesignerSerializationManager manager, object codeObject)
    {
    object ret = null;
    if( manager == null )
    throw new ArgumentNullException("manager");
    if( !(codeObject is CodeStatementCollection) )
    throw new ArgumentException("Invalid type (expected: CodeStatementCollection)", "codeObject");

    Assembly a = typeof(System.ComponentModel.Design.Serialization.CodeDomSerializer).Assembly;
    CodeDomSerializer s;
    if (a.GetName(false).Version.Major < 2)
    {
    s = (CodeDomSerializer)a.CreateInstance("System.ComponentModel.Design.Serialization.InstanceDescriptorCodeDomSerializer", false, BindingFlags.CreateInstance, null, null, null, null);
    }
    else
    { // 2.0 framework and later, CodeDomSerialize is not abstract
    s = (CodeDomSerializer)manager.GetSerializer(typeof(InstanceDescriptor), typeof(CodeDomSerializer));
    }
    if( s != null )
    ret = s.Deserialize(manager, codeObject);
    return ret;
    }
    public override object Serialize(IDesignerSerializationManager manager, object value)
    {
    object ret = null;
    if( manager == null )
    throw new ArgumentNullException("manager");

    string name = manager.GetName(value);
    if (name != null)
    return new CodeVariableReferenceExpression(name);

    Assembly a = typeof(System.ComponentModel.Design.Serialization.CodeDomSerializer).Assembly;
    CodeDomSerializer s;
    if (a.GetName(false).Version.Major < 2)
    {
    s = (CodeDomSerializer)a.CreateInstance("System.ComponentModel.Design.Serialization.InstanceDescriptorCodeDomSerializer", false, BindingFlags.CreateInstance, null, null, null, null);
    }
    else
    { // 2.0 framework and later, CodeDomSerialize is not abstract
    s = (CodeDomSerializer)manager.GetSerializer(typeof(InstanceDescriptor), typeof(CodeDomSerializer));
    }
    if (s != null)
    ret = s.Serialize(manager, value);
    return ret;
    }
    }
    } // namespace PropertyGridBugSampleControl

    Here is the code for the test application:

    using System;
    using System.Drawing;
    using System.Collections;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Data;

    namespace PropertyGridBugSampleApp
    {
    /// <summary>
    /// Summary description for Form1.
    /// </summary>
    public class Form1 : System.Windows.Forms.Form
    {
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl1;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl2;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl3;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl4;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl5;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl6;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl7;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl8;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl9;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl10;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl11;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl12;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl13;
    private PropertyGridBugSampleControl.PropertyGridBugSampleControl propertyGridBugSampleControl14;
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.Container components = null;

    public Form1()
    {
    //
    // Required for Windows Form Designer support
    //
    InitializeComponent();

    //
    // TODO: Add any constructor code after InitializeComponent call
    //
    }

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    protected override void Dispose( bool disposing )
    {
    if( disposing )
    {
    if (components != null)
    {
    components.Dispose();
    }
    }
    base.Dispose( disposing );
    }

    #region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
    this.propertyGridBugSampleControl1 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl2 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl3 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl4 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl5 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl6 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl7 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl8 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl9 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl10 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl11 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl12 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl13 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.propertyGridBugSampleControl14 = new PropertyGridBugSampleControl.PropertyGridBugSampleControl();
    this.SuspendLayout();
    //
    // propertyGridBugSampleControl1
    //
    this.propertyGridBugSampleControl1.Location = new System.Drawing.Point(8, 8);
    this.propertyGridBugSampleControl1.Name = "propertyGridBugSampleControl1";
    this.propertyGridBugSampleControl1.SampleProperty = null;
    this.propertyGridBugSampleControl1.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl1.TabIndex = 0;
    this.propertyGridBugSampleControl1.Text = "propertyGridBugSampleControl1";
    //
    // propertyGridBugSampleControl2
    //
    this.propertyGridBugSampleControl2.Location = new System.Drawing.Point(8, 48);
    this.propertyGridBugSampleControl2.Name = "propertyGridBugSampleControl2";
    this.propertyGridBugSampleControl2.SampleProperty = null;
    this.propertyGridBugSampleControl2.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl2.TabIndex = 1;
    this.propertyGridBugSampleControl2.Text = "propertyGridBugSampleControl2";
    //
    // propertyGridBugSampleControl3
    //
    this.propertyGridBugSampleControl3.Location = new System.Drawing.Point(8, 88);
    this.propertyGridBugSampleControl3.Name = "propertyGridBugSampleControl3";
    this.propertyGridBugSampleControl3.SampleProperty = null;
    this.propertyGridBugSampleControl3.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl3.TabIndex = 2;
    this.propertyGridBugSampleControl3.Text = "propertyGridBugSampleControl3";
    //
    // propertyGridBugSampleControl4
    //
    this.propertyGridBugSampleControl4.Location = new System.Drawing.Point(8, 128);
    this.propertyGridBugSampleControl4.Name = "propertyGridBugSampleControl4";
    this.propertyGridBugSampleControl4.SampleProperty = null;
    this.propertyGridBugSampleControl4.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl4.TabIndex = 3;
    this.propertyGridBugSampleControl4.Text = "propertyGridBugSampleControl4";
    //
    // propertyGridBugSampleControl5
    //
    this.propertyGridBugSampleControl5.Location = new System.Drawing.Point(8, 168);
    this.propertyGridBugSampleControl5.Name = "propertyGridBugSampleControl5";
    this.propertyGridBugSampleControl5.SampleProperty = null;
    this.propertyGridBugSampleControl5.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl5.TabIndex = 7;
    this.propertyGridBugSampleControl5.Text = "propertyGridBugSampleControl5";
    //
    // propertyGridBugSampleControl6
    //
    this.propertyGridBugSampleControl6.Location = new System.Drawing.Point(8, 208);
    this.propertyGridBugSampleControl6.Name = "propertyGridBugSampleControl6";
    this.propertyGridBugSampleControl6.SampleProperty = null;
    this.propertyGridBugSampleControl6.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl6.TabIndex = 6;
    this.propertyGridBugSampleControl6.Text = "propertyGridBugSampleControl6";
    //
    // propertyGridBugSampleControl7
    //
    this.propertyGridBugSampleControl7.Location = new System.Drawing.Point(8, 248);
    this.propertyGridBugSampleControl7.Name = "propertyGridBugSampleControl7";
    this.propertyGridBugSampleControl7.SampleProperty = null;
    this.propertyGridBugSampleControl7.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl7.TabIndex = 5;
    this.propertyGridBugSampleControl7.Text = "propertyGridBugSampleControl7";
    //
    // propertyGridBugSampleControl8
    //
    this.propertyGridBugSampleControl8.Location = new System.Drawing.Point(104, 8);
    this.propertyGridBugSampleControl8.Name = "propertyGridBugSampleControl8";
    this.propertyGridBugSampleControl8.SampleProperty = null;
    this.propertyGridBugSampleControl8.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl8.TabIndex = 4;
    this.propertyGridBugSampleControl8.Text = "propertyGridBugSampleControl8";
    //
    // propertyGridBugSampleControl9
    //
    this.propertyGridBugSampleControl9.Location = new System.Drawing.Point(104, 48);
    this.propertyGridBugSampleControl9.Name = "propertyGridBugSampleControl9";
    this.propertyGridBugSampleControl9.SampleProperty = null;
    this.propertyGridBugSampleControl9.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl9.TabIndex = 8;
    this.propertyGridBugSampleControl9.Text = "propertyGridBugSampleControl9";
    //
    // propertyGridBugSampleControl10
    //
    this.propertyGridBugSampleControl10.Location = new System.Drawing.Point(104, 88);
    this.propertyGridBugSampleControl10.Name = "propertyGridBugSampleControl10";
    this.propertyGridBugSampleControl10.SampleProperty = null;
    this.propertyGridBugSampleControl10.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl10.TabIndex = 9;
    this.propertyGridBugSampleControl10.Text = "propertyGridBugSampleControl10";
    //
    // propertyGridBugSampleControl11
    //
    this.propertyGridBugSampleControl11.Location = new System.Drawing.Point(104, 128);
    this.propertyGridBugSampleControl11.Name = "propertyGridBugSampleControl11";
    this.propertyGridBugSampleControl11.SampleProperty = null;
    this.propertyGridBugSampleControl11.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl11.TabIndex = 10;
    this.propertyGridBugSampleControl11.Text = "propertyGridBugSampleControl11";
    //
    // propertyGridBugSampleControl12
    //
    this.propertyGridBugSampleControl12.Location = new System.Drawing.Point(104, 168);
    this.propertyGridBugSampleControl12.Name = "propertyGridBugSampleControl12";
    this.propertyGridBugSampleControl12.SampleProperty = null;
    this.propertyGridBugSampleControl12.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl12.TabIndex = 11;
    this.propertyGridBugSampleControl12.Text = "propertyGridBugSampleControl12";
    //
    // propertyGridBugSampleControl13
    //
    this.propertyGridBugSampleControl13.Location = new System.Drawing.Point(104, 208);
    this.propertyGridBugSampleControl13.Name = "propertyGridBugSampleControl13";
    this.propertyGridBugSampleControl13.SampleProperty = null;
    this.propertyGridBugSampleControl13.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl13.TabIndex = 12;
    this.propertyGridBugSampleControl13.Text = "propertyGridBugSampleControl13";
    //
    // propertyGridBugSampleControl14
    //
    this.propertyGridBugSampleControl14.Location = new System.Drawing.Point(104, 248);
    this.propertyGridBugSampleControl14.Name = "propertyGridBugSampleControl14";
    this.propertyGridBugSampleControl14.SampleProperty = null;
    this.propertyGridBugSampleControl14.Size = new System.Drawing.Size(88, 24);
    this.propertyGridBugSampleControl14.TabIndex = 13;
    this.propertyGridBugSampleControl14.Text = "propertyGridBugSampleControl14";
    //
    // Form1
    //
    this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
    this.ClientSize = new System.Drawing.Size(292, 302);
    this.Controls.AddRange(new System.Windows.Forms.Control[] {
    this.propertyGridBugSampleControl14,
    this.propertyGridBugSampleControl13,
    this.propertyGridBugSampleControl12,
    this.propertyGridBugSampleControl11,
    this.propertyGridBugSampleControl10,
    this.propertyGridBugSampleControl9,
    this.propertyGridBugSampleControl5,
    this.propertyGridBugSampleControl6,
    this.propertyGridBugSampleControl7,
    this.propertyGridBugSampleControl8,
    this.propertyGridBugSampleControl4,
    this.propertyGridBugSampleControl3,
    this.propertyGridBugSampleControl2,
    this.propertyGridBugSampleControl1});
    this.Name = "Form1";
    this.Text = "Form1";
    this.ResumeLayout(false);

    }
    #endregion

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
    Application.Run(new Form1());
    }
    }
    }

    Tuesday, June 5, 2007 5:15 PM
  • Hi,

    you say that my solution won't work but when I see your code it won't work simply because your type is cloneable and therefore, in CopyValue, it will never try to get an InstanceDescriptor.

    I won't be able to help about code serialization because I'm not knowledgeable enough in this area. What I can say for your sample is that, when I add for each SamplePropertyTypeX class the missing Equals(...) methods, it works perfectly with the MS PropertyGrid at runtime (with several selected objects), except of course that it clones the new property value (and here my solution could help). My feeling is that there is an issue with your serializer (or deeper with the MS code) and again I can't help for that.

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker

    Tuesday, June 5, 2007 7:27 PM
  • Nicolas,

    The sample does work in Visual Studio 2002 and 2003.  It only has the problem in Visual Studio 2005.

    I tried taking your code and adding it to my sample, to see if I could make your suggestion work, and I was not able to do so.  The problem is, your example does not have all of the design time support that mine does.  Your type converter is more of a run-time type converter, while mine is specifically designed for use in design mode, and when I change your code to have the additional things that mine does, like GetPropertiesSupported, GetProperties, CreateInstanceSupported, CreateInstance, and a ConvertFrom that can convert to string, then it exhibits the same problem.  It all comes back to that CopyValue code; if the type being assigned cannot be copied, then the problem simply goes away, since CopyValue will return the same instance in the case where it cannot copy the value.  If there is any of those four ways I listed in my previous post to copy the object, then CopyValue will do so and cause this bug.

    Unfortunately, removing the ICloneable implementation is not an option, nor is making the classes not serializable.  The classes in question have been implementing ICloneable and marked with SerializableAttribute for several major versions of the product (by the way, the product is FarPoint Spread for Windows Forms, in case you are interested).

    I just noticed today that the problem is very evident when using our product to edit NamedStyleCollection objects.  We are using a collection editor inheriting from the framework's, and letting the collection editor form handle dealing with the property grid, and editing NamedStyle.CellType in that editor is a perfect example of why and how this is broken.  In Visual Studio 2002 and 2003, it will work to select a set of NamedStyle objects and then set their CellType property to some cell type, but in Visual Studio 2005 it has this problem.

    Calling all Microsoft PropertyGrid developers . . . . . can you verify if this is the intended behavior of the PropertyGrid or explain why it is not a bug?  I am becoming more and more convinced that this is indeed a bug, and a nasty one that I cannot work around at that.

    I am sure it must be related to that problem I worked around in .Net 1.0 with InstanceDescriptorCodeDomSerializer (it is a bad solutiuon to that problem, however; there is a MUCH better one, just look at the code in my post above).

    Thanks to all who have looked in to this for me and with me.
    Tuesday, June 5, 2007 8:53 PM
  • You do right to call Microsoft developers and you should even report this bug officially. However I fear you won't get a fix for .Net 2.x...
    I will update my article so that it's more clear that the type of solution I gave is for runtime issues and not for the complex designtime issue you have.

    Good luck !

    Best regards,

    Nicolas Cadilhac
    Smart PropertyGrid.Net @ VisualHint
    Microsoft PropertyGrid Resource List
    Free PropertyGrid for MFC
    Smart FieldPackEditor.Net / DateTimePicker

    Tuesday, June 5, 2007 9:05 PM
  • I have found a workaround that is not too bad.  If you implement ICloneable in the object but put code in it that will not let the property grid's MergePropertyDescriptor.CopyValue method actually clone the object, then it will work around the problem with minimal coding:

        public virtual object Clone()
        {
          StackFrame frame = new StackFrame(1);
          MethodInfo methodInfo = frame.GetMethod() as MethodInfo;
          if( methodInfo != null && methodInfo.Name.Equals("CopyValue") && methodInfo.DeclaringType.Name.Equals("MergePropertyDescriptor") )
            return this;
          return MemberwiseClone();
        }

    This workaround should not have any other side effects either, since it should only affect the property grid, and it should take care of both design time and run time issues.
    Wednesday, June 6, 2007 3:31 PM
  • Here is a slight improvement to that workaround that will work for cases where the class has other classes inheriting it and overriding the Clone method:

        public virtual object Clone()
        {
          StackFrame frame = new StackFrame(1);
          MethodInfo methodInfo = frame.GetMethod() as MethodInfo;
          for (int n = 1; methodInfo != null && methodInfo.Name.Equals("Clone"); )
          {
            frame = new StackFrame(++n);
            methodInfo = frame.GetMethod() as MethodInfo;
          }
          if (methodInfo != null && methodInfo.Name.Equals("CopyValue") && methodInfo.DeclaringType.Name.Equals("MergePropertyDescriptor"))
            return this;
          return MemberwiseClone();
        }


    Inheriting classes that override the Clone method should check whether the base class returns this and also do so:

          if( base.Clone() == this )
            return this;

    Thursday, June 7, 2007 5:00 PM