IExtenderProvider add just some properties depending on object type
Asked Answered
T

1

3

I'm having an issue and I don't know if this is indeed doable (if there's a "hacky" way, I'm all up for it, but I haven't found one).

I have an IExtenderProvider component that I'm using to have my own UITypeEditor for some properties on third-party controls (which I can't change, for obvious reasons).

These controls don't necessarily inherit from the same base (and if they do, the base doesn't necessarily have the properties I want to extend, and those are defined in the same class).

So, imagine for example I want to make an alternative property for the properties Image, Glyph, LargeGlyph, SmallGlyph on them.

So I have something like:

[ProvideProperty("LargeGlyphCustom", typeof (object))]
[ProvideProperty("GlyphCustom", typeof(object))]
[ProvideProperty("SmallImageCustom", typeof(object))]
[ProvideProperty("LargeImageCustom", typeof(object))]
[ProvideProperty("ImageCustom", typeof(object))]
public class MyImageExtender : Component, IExtenderProvider
{
  private readonly Type[] _extendedTypes =
  {
    typeof (OtherControl),
    typeof (SomeOtherControl),
    typeof (AControl),
    typeof (AButton)
  };

  bool IExtenderProvider.CanExtend(object o)
  {
    if (!DesignMode) return false;
    return _extendedTypes.Any(t => t.IsInstanceOfType(o));
  } 

  // Implement the property setter and getter methods
}

So far, so good. I can see my properties on the controls of the types I'm expecting.

However, these are replacements (just to change the UITypeEditor) of properties in the control.

The problem with my approach is that I see all of the extended properties in all of the extended types.

Say, if AButton only has Image, I only want to see ImageCustom and not SmallImageCustom, LargeImageCustom, etc.

So my approach was to do this:

[ProvideProperty("LargeGlyphCustom", typeof (OtherControl))]
// other properties
[ProvideProperty("ImageCustom", typeof(AButton))]
public class MyImageExtender : Component, IExtenderProvider
// ...

This seemed to work fine, and now I only see ImageCustom on AButton, and LargeGlyphCustom on OtherControl.

Now the problem is, if I want to show ImageCustom in both AButton and OtherControl, I had thought of doing this:

[ProvideProperty("ImageCustom", typeof(AButton))]
[ProvideProperty("ImageCustom", typeof(OtherControl))]
public class MyImageExtender : Component, IExtenderProvider

This doesn't work though, I only get to see ImageCustom on AButton, but not on OtherControl.

Decompiling the sources for ProvidePropertyAttribute, the reason this happens is "arguably" clear. It internally creates a TypeId, which I suspect is what the WinForms designer is using like this:

public override object TypeId
{
  get
  {
    return (object) (this.GetType().FullName + this.propertyName);
  }
}

Which makes the TypeId be "ProvidePropertyAttributeImageCustom", so it can't differentiate between the different receiver types.

I'm going to test deriving ProvidePropertyAttribute and create a different TypeId since it seems overridable, but I expect the winforms designer expect the specific ProvidePropertyAttribute type and not a derived one (the winforms designer is picky with these things).

Ouch, ProvidePropertyAttribute is sealed so I can't derive and make my custom TypeId, it seems (not that I had high hopes that this would work at all)

In the meantime, anyone has ever done something like this and know something I could use?

Twiddle answered 29/12, 2015 at 11:4 Comment(2)
Are the controls that you want to extend, your custom controls?Woundwort
@RezaAghaei nope, as stated on the question, they are third-party (and I don't have the source code)Twiddle
T
5

I know this is a quick answer, but this has been driving me nuts for a few days, so I've gone a different route which seems to work just fine.

Since the target goal (as I explained on my question) was to change the UITypeEditor on some properties, I've made a non-visual component that overrides the attributes (using a custom TypeDescriptor) on those properties, and assign my custom UITypeEditor there.

I used this answer as a base for implementing the property-overriding TypeDescriptor.

Update

For the record, the solution provided in the linked answer worked, however it had a problem where the TypeDescriptionProvider would get picked up for derived classes, however the returned TypeDescriptor would only return the properties for the base object (the one for which you passed in the parent TypeDescriptor), causing havok in things like the winforms designer.

I made an all purpose property-overrider TypeDescriptionProvider. So far, it has worked just fine. Here's the implementation. See the linked answer for an explanation of where did this come from:

  1. The provider:

    internal class PropertyOverridingTypeDescriptionProvider : TypeDescriptionProvider
    {
        private readonly Dictionary<Type, ICustomTypeDescriptor> _descriptorCache = new Dictionary<Type, ICustomTypeDescriptor>();
        private readonly Func<PropertyDescriptor, bool> _condition;
        private readonly Func<PropertyDescriptor, Type, PropertyDescriptor> _propertyCreator;
    
        public PropertyOverridingTypeDescriptionProvider(TypeDescriptionProvider parentProvider, Func<PropertyDescriptor, bool> condition, Func<PropertyDescriptor, Type, PropertyDescriptor> propertyCreator) : base(parentProvider)
        {
            _condition = condition;
            _propertyCreator = propertyCreator;
        }
    
        public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
        {
            lock (_descriptorCache)
            {
                ICustomTypeDescriptor returnDescriptor;
                if (!_descriptorCache.TryGetValue(objectType, out returnDescriptor))
                {
                    returnDescriptor = CreateTypeDescriptor(objectType);
                }
                return returnDescriptor;
            }
        }
    
        private ICustomTypeDescriptor CreateTypeDescriptor(Type targetType)
        {
            var descriptor = base.GetTypeDescriptor(targetType, null);
            _descriptorCache.Add(targetType, descriptor);
            var ctd = new PropertyOverridingTypeDescriptor(descriptor, targetType, _condition, _propertyCreator);
            _descriptorCache[targetType] = ctd;
            return ctd;
        }
    }
    
  2. This is the actual TypeDescriptor:

    internal class PropertyOverridingTypeDescriptor : CustomTypeDescriptor
    {
        private readonly ICustomTypeDescriptor _parent;
        private readonly PropertyDescriptorCollection _propertyCollection;
        private readonly Type _objectType;
        private readonly Func<PropertyDescriptor, bool> _condition;
        private readonly Func<PropertyDescriptor, Type, PropertyDescriptor> _propertyCreator;
    
        public PropertyOverridingTypeDescriptor(ICustomTypeDescriptor parent, Type objectType, Func<PropertyDescriptor, bool> condition, Func<PropertyDescriptor, Type, PropertyDescriptor> propertyCreator)
            : base(parent)
        {
            _parent = parent;
            _objectType = objectType;
            _condition = condition;
            _propertyCreator = propertyCreator;
            _propertyCollection = BuildPropertyCollection();
        }
    
        private PropertyDescriptorCollection BuildPropertyCollection()
        {
            var isChanged = false;
            var parentProperties = _parent.GetProperties();
    
            var pdl = new PropertyDescriptor[parentProperties.Count];
            var index = 0;
            foreach (var pd in parentProperties.OfType<PropertyDescriptor>())
            {
                var pdReplaced = pd;
                if (_condition(pd))
                {
                    pdReplaced = _propertyCreator(pd, _objectType);
                }
                if (!ReferenceEquals(pdReplaced, pd)) isChanged = true;
                pdl[index++] = pdReplaced;
            }
            return !isChanged ? parentProperties : new PropertyDescriptorCollection(pdl);
        }
    
        public override object GetPropertyOwner(PropertyDescriptor pd)
        {
            var o = base.GetPropertyOwner(pd);
            return o ?? this;
        }
    
        public override PropertyDescriptorCollection GetProperties()
        {
            return _propertyCollection;
        }
        public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            return _propertyCollection;
        }
    }
    

And here's how you use it. I've commented this:

private void ChangeTypeProperties(Type modifiedType, params string[] propertyNames)
{
    // Get the current TypeDescriptionProvider
    var curProvider = TypeDescriptor.GetProvider(modifiedType);
    // Create a replacement provider, pass in the parent, this is important
    var replaceProvider = new PropertyOverridingTypeDescriptionProvider(curProvider,
        // This the predicate that says wether a `PropertyDescriptor` should be changed
        // Here we are changing only the System.Drawing.Image properties,
        // either those whose name we pass in, or all if we pass none
        pd =>
            typeof (System.Drawing.Image).IsAssignableFrom(pd.PropertyType) &&
            (propertyNames.Length == 0 || propertyNames.Contains(pd.Name)),

        // This our "replacer" function. It'll get the source PropertyDescriptor and the object type.
        // You could use pd.ComponentType for the object type, but I've
        // found it to fail under some circumstances, so I just pass it
        // along
        (pd, t) =>
        {
            // Get original attributes except the ones we want to change
            var atts = pd.Attributes.OfType<Attribute>().Where(x => x.GetType() != typeof (EditorAttribute)).ToList();
            // Add our own attributes
            atts.Add(new EditorAttribute(typeof (MyOwnEditor), typeof (System.Drawing.Design.UITypeEditor)));
            // Create the new PropertyDescriptor
            return TypeDescriptor.CreateProperty(t, pd, atts.ToArray());
        }
    );
    // Finally we replace the TypeDescriptionProvider
    TypeDescriptor.AddProvider(replaceProvider, modifiedType);
}

Now, for the requirements of my question, I've created a simple drop-in component which I drop on the base form, which does just this:

public class ToolbarImageEditorExtender : Component
{
    private static bool _alreadyInitialized;
    public ToolbarImageEditorExtender()
    {
        // no need to reinitialize if we drop more than one component
        if (_alreadyInitialized)
            return;
        _alreadyInitialized = true;
        // the ChangeTypeProperties function above. I just made a generic version
        ChangeTypeProperties<OtherControl>(nameof(OtherControl.Glyph), nameof(OtherControl.LargeGlyph));
        ChangeTypeProperties<AButton>(nameof(AButton.SmallImage), nameof(AButton.LargeImage));
        // etc.
    }
}

So far, it has worked wonders.

Twiddle answered 29/12, 2015 at 12:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.