views:

486

answers:

5

I have a property on my World class that looks like this:

    public Vec2 Gravity
    {
        get {
            Console.WriteLine("Getting gravity!");
            return GetGravity(); 
        }
        set {
            Console.WriteLine("Setting gravity!");
            SetGravity(value); 
        }
    }

The "Getting gravity!" string is displayed as expected, when the PropertyGrid tries to read the value, but when I try to change the gravity vector and press enter, nothing happens. Why not?


My Vec2 class has properties:

    public float X
    {
        get
        {
            return x;
        }
        set
        {
            x = value;
        }
    }

    public float Y
    {
        get
        {
            return y;
        }
        set
        {
            y = value;
        }
    }

Which are visible on the grid, thanks to:

public class Vec2Converter : ExpandableObjectConverter
{
    public override bool CanConvertTo(ITypeDescriptorContext context, System.Type destinationType)
    {
        if (destinationType == typeof(Vec2)) return true;
        else return base.CanConvertTo(context, destinationType);
    }

    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, System.Type destinationType)
    {
        if (destinationType == typeof(string) && value is Vec2)
        {
            Vec2 vec = (Vec2)value;
            return string.Format("{0}, {1}", vec.X, vec.Y);
        }
        else return base.ConvertTo(context, culture, value, destinationType);
    }
}

I just added these two methods to the Vec2Converter and now it works:

    public override bool CanConvertFrom(ITypeDescriptorContext context, System.Type sourceType)
    {
        if (sourceType == typeof(string)) return true;
        else return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        if (value is string)
        {
            try
            {
                string strVal = value as string;
                var parts = strVal.Split(',');
                float x = float.Parse(parts[0]);
                float y = float.Parse(parts[1]);
                return new Vec2(x, y);
            }
            catch
            {
                throw new ArgumentException("Can not convert '" + (string)value + "'to type Vec2");
            }
        }
        else return base.ConvertFrom(context, culture, value);
    }

It works both by changing the string representation, and the individual properties. Why does thix fix it for the case with individual properties??


I think the answer is that Vec2 is a struct, so when World returns the Gravity vector, it passes it by value, and then the copy is changed, rather than the actual Gravity vector.

The solution, I think is either to keep the CanConvertFrom and ConvertFrom methods in place. I'm assuming those make it work because they modify the string representation of the gravity vector, and then that in turn updates the actual gravity vector. That, or making Vec2 a class ought to work, but I can't really test that now because my app is very dependent on it being a struct.

A: 

The answer depends on how you are editing the property:

If you edit the property by typing in text directly into the Gravity property then I suspect your TypeConverter will be called and the Gravity property's setter should be called. This is assuming that the Gravity property even displays as editable in the property grid, which I suspect it should.

If you edit the property by expanding the property (clicking the little + sign) and then directly setting its X or Y property then the Gravity property's setter will never be called. Just the X and Y properties will have their setters get called.

In general and in most frameworks complex properties are not settable. They are often set only via their sub-properties. In this case I do think it's reasonable to have the entire property settable since it seems like it would be a common operation. It's just a design choice and it's neither right nor wrong to do it either way.

Eilon
You had me up until the last paragraph. I was expanding the the property and setting the individual X and Y properties. The string representation of gravity was not editable (I didn't have those methods implemented), and that's how I wanted it. But I assumed the string rep would be updated when I edited the sub-properties. How would I get it to do that?
Mark
@Mark, Apply the `NotifyParentProperty(true)` attribute to the `X` and `Y` properties. That should cause it to be refreshed. Feel free to ignore my last paragraph :)
Eilon
Just FYI, the `NotifyParentProperty(true)` attribute doesn't appear to do anything.
Mark
+1  A: 

I think the property grid is setting the properties directly on the Vec2 object (i.e. Xand Y properties).

MartinStettner
Good call. It was indeed changing the `Vec2` without calling the `Gravity` setter. But why? And how would I fix that?
Mark
If I only knew :-) I had a very similar problem a few days ago but it wasn't important enough for me to dig for a solution...
MartinStettner
@Mark, With what you have you can't get it to call the setter because it doesn't need to call the setter. What would be the point of VS creating a new instance of `Vec3` when it just wants to set one of its sub-properties? To make it always call it you'll have to get rid of the `ExpandableObjectConverter` and have the `Gravity` property be editable only by typing in `"123, 456"` in the property grid.
Eilon
Btw. where exactly is the problem with setting the X and Y (sub-)properties instead of the Gravity property? Have you tried to make Vec2 a struct? I have no idea if this would work, but if it's an option for you... though you should remove the setters in this case
MartinStettner
@Martin: Vec2 already is a struct. The problem is that editing the sub-properties edits the `Vec2` that is returned by the `Gravity` getter, which does *not* refer to the `_gravity` variable on my `World` object. i.e., changing the X/Y properties is doing *something* but those changes are discarded immediately and it's quite useless. @Eilon: Well that's what it seems to be doing (creating/copying a new instance) -- *light turns on* -- It must be because `Vec2` is a struct.
Mark
Therefore: think twice before adding property setters to a struct :-) (Don't remember, where I read it, but it's not from me ...)
MartinStettner
And pleaase, could someone explain, why Eilon's and my answers were downvoted. Not that I really care, but it's such a silly thing (and admittingly somewhat annoying ...)
MartinStettner
@Martin: Wasn't me. I upvoted.
Mark
+1  A: 

It is changing the X and Y properties of Vec2, because that's what you are setting. When changing these properties, if you translate it into code, I'm guessing that it is calling Gravity.X = value;, which, when you think about it, is really a Get on Gravity, and then a Set on Vec2.X.

With your current code, in order to enter the Set of Gravity, you would need to explicitly change it to a new Vec2 object.

EDIT: And apparently, [NotifyParentProperty(true)] is no good, clearing up my uncertainty.

Dynami Le Savard
No, this attribute is used to trigger the OnComponentChanging and OnComponentChanged calls on an instance of IComponentChangeService.
Nicolas Cadilhac
Well, that doesn't make much sense, because the `X` and `Y` values of `Gravity` weren't changing at all, nevermind the setter.
Mark
Well, since Vec2 is a struct, `Gravity.Get` will return a copy, not a reference to your actual Gravity, and the following Set on `Vec2.X` will change the value of the copy, not the actual value you have stored.
Dynami Le Savard
+1  A: 

Here is the code for PropertyDescriptorGridEntry.SetPropertyValueCore:

protected void SetPropertyValueCore(object obj, object value, bool doUndo)
{
    if (this.propertyInfo != null)
    {
        Cursor current = Cursor.Current;
        try
        {
            Cursor.Current = Cursors.WaitCursor;
            object component = obj;
            if (component is ICustomTypeDescriptor)
            {
                component = ((ICustomTypeDescriptor) component).GetPropertyOwner(this.propertyInfo);
            }
            bool flag = false;
            if (this.ParentGridEntry != null)
            {
                Type propertyType = this.ParentGridEntry.PropertyType;
                flag = propertyType.IsValueType || propertyType.IsArray;
            }
            if (component != null)
            {
                this.propertyInfo.SetValue(component, value);
                if (flag)
                {
                    GridEntry parentGridEntry = this.ParentGridEntry;
                    if ((parentGridEntry != null) && parentGridEntry.IsValueEditable)
                    {
                        parentGridEntry.PropertyValue = obj;
                    }
                }
            }
        }
        finally
        {
            Cursor.Current = current;
        }
    }
}

In your case Vec2 is a value type, so flag is true. In the grid, the vec2 instance is a copy of your struct, so your original has not been modified yet but when parentGridEntry.PropertyValue = obj; is called, then the value is assigned through the PropertyDescriptor of the Gravity property in your container class. The fact that you added CanConvertFrom instructs the grid that parentGridEntry.IsValueEditable is now true.

Nicolas Cadilhac