Question

Context

I've been working on a custom collection editor / designer for a custom ASP.Net web control. The web control exposes a strange hierarchy, so a custom editor seemed like the right thing to do to make it easier for developers.

Building ASPX code and using the web control works. In other words, things like PersistChildren and ParseChildren are taken care of.

The signature of the property in the web control looks something like this:

[PersistenceMode(PersistenceMode.InnerProperty)]
[Themeable(false)]
[Browsable(false)]
public virtual DimensionsCollection Dimensions { get; internal set; }

Note that the property is not public; if it were public, all kinds of things in the designer will go wrong. DimensionsCollection is a class that simply inherits List<Dimension>. The Dimension class itself is nothing fancy, just a thing with some properties.

Just because I think it looks cool, I want to be able to modify the property from an action in the designer. To do that, I implemented a ControlDesigner class and added an ActionList. One of the actions there is a linkbutton that opens an editor:

var editor = new Editors.DimensionEditor(control.Dimensions);
if (editor.ShowDialog() == DialogResult.OK)
{ /* SEE BELOW */ }

The editor itself is a windows form that takes a List<Dimension> as constructor argument and modifies the collection.

Problem

When I use this code, I can see that the editor works and that the control collection is updated in the 'designer' view. If I open the editor multiple times, the state changes, meaning that somewhere in memory the state is updated by the editor.

However, if I go to the ASPX code, I can see that the Dimensions are not there anymore. So, the problem in a nutshell is that I somehow have to tell Visual Studio to write/serialize/persist the property to the ASPX file. (simple as that...)

Strangely, I cannot find anywhere how to do this... even though a normal CollectionEditor seems to be capable of doing just that (which I cannot subclass unfortunately)

Some things I tried

For other properties I noticed you have to use something like this, but this doesn't seem to work. Code was entered at the point marked as 'see below' or in some cases to a helper call in the designer called from that point:

PropertyDescriptor pd = TypeDescriptor.GetProperties(base.Component)["Dimensions"];
// use setter with internal property -> no effect
// this.OnComponentChanged(this, new ComponentChangedEventArgs(this.Component, pd, null, newdim)); -> no effect
// use getter to obtain list -> populate that using another list that's created in the editor

I can understand why it doesn't work; apparently someone has to tell Visual Studio that the property has changed... I just don't know how to do just that.

Was it helpful?

Solution

This was really a pain to figure out with apparently no sources online that explain how to do this.

Basically you want to use the OnComponentChanging / Changed methods to notify the designer. And apparently the designer uses transactions for the rest of the logic. (My guess is that it has to do with undo/redo behavior). For a normal type this is done automatically when you use the PropertyDescriptor, for collections it apparently doesn't wrap the collection which means you have to do it manually.

To solve the issue, you need to create a small method like this in either the UITypeEditor or in the DesignerActionList class your implementing:

private void ChangeAction(List<Dimension> newDimensions)
{
    IDesignerHost host = GetService(typeof(IDesignerHost)) as IDesignerHost;
    PropertyDescriptor pd = TypeDescriptor.GetProperties(typeof(MyControl))["Dimensions"];
    var dimensions = (DimensionsCollection)pd.GetValue(control);

    var trans = host.CreateTransaction();
    IComponentChangeService ccs = (IComponentChangeService)GetService(typeof(IComponentChangeService));
    ccs.OnComponentChanging(control, pd);

    dimensions.Clear();
    dimensions.AddRange(newDimensions);

    ccs.OnComponentChanged(control, pd, null, dimensions);
    trans.Commit();
}

If you're implementing a UITypeEditor, make sure to use context.Instance from EditValue as the control and the given provider to lookup the services.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top